WeakMap und WeakReference in PHP: Caches ohne Memory Leaks bauen
AI generated
<?php
8.4
PHP · WeakMap · Speicherverwaltung · Caching
WeakMap und WeakReference in PHP
Caches bauen, ohne Objekte künstlich am Leben zu halten

Ein gewöhnliches Array als Cache mit Objekten als Schlüsseln oder Werten verhindert, dass diese Objekte jemals vom Garbage Collector eingesammelt werden, solange der Cache selbst existiert, und genau das führt in lang laufenden Prozessen zu schleichenden Memory Leaks. WeakMap löst dieses Problem strukturell, indem der Cache selbst keinen Einfluss mehr auf die Lebensdauer der referenzierten Objekte nimmt.

14 Min. Lesezeit WeakMap · WeakReference · Speicher PHP 8.x

1. Warum gewöhnliche Caches Objekte künstlich am Leben halten

Ein naheliegendes Muster, um teure Berechnungen zu einem Objekt zwischenzuspeichern, ist ein assoziatives Array oder ein SplObjectStorage, das ein Objekt als Schlüssel und das Berechnungsergebnis als Wert verwendet. Das Problem dabei: Solange der Cache selbst existiert, hält er eine starke Referenz auf jedes eingetragene Objekt, wodurch dessen Referenzzähler nie auf null fällt, selbst wenn der Rest der Anwendung längst keine andere Referenz mehr auf dieses Objekt besitzt.

In kurzlebigen PHP-Prozessen, wie einem klassischen PHP-FPM-Request, fällt dieses Problem selten auf, weil der gesamte Speicher am Ende des Requests ohnehin freigegeben wird. In lang laufenden Prozessen dagegen, etwa Worker-Prozessen mit Swoole, RoadRunner oder langlebigen CLI-Kommandos, die tausende Objekte über die Laufzeit hinweg verarbeiten, wächst ein solcher Cache unkontrolliert, weil jedes verarbeitete Objekt dauerhaft im Cache verbleibt und damit vom Garbage Collector niemals eingesammelt werden kann.

WeakMap und WeakReference, beide seit PHP 8.0 beziehungsweise 7.4 verfügbar, lösen genau dieses Problem. Sie erlauben es, ein Objekt zu referenzieren, ohne dessen Referenzzähler zu erhöhen, sodass der Garbage Collector das Objekt einsammeln kann, sobald keine starke Referenz mehr existiert, unabhängig davon, ob es noch in einer WeakMap oder als WeakReference vermerkt ist.

2. WeakReference: ein einzelnes Objekt schwach referenzieren

WeakReference ist die einfachere der beiden Klassen und eignet sich für den Fall, dass ein einzelnes Objekt beobachtet werden soll, ohne dessen Lebensdauer zu beeinflussen. Mit WeakReference::create($objekt) entsteht ein Wrapper, dessen Methode get() entweder das Original-Objekt zurückgibt, solange es noch lebt, oder null, sobald es vom Garbage Collector eingesammelt wurde. Die WeakReference selbst zählt dabei nicht als Referenz im Sinne des Referenzzählers.

Ein typischer Anwendungsfall ist ein Beobachter, der über den Lebenszyklus eines Objekts informiert werden soll, ohne selbst dessen Existenz zu erzwingen, etwa ein Debugging-Werkzeug, das prüft, ob ein bestimmtes Objekt zum erwarteten Zeitpunkt bereits freigegeben wurde. Da get() jederzeit null liefern kann, muss jeder Zugriff auf eine WeakReference diesen Fall explizit behandeln, ein einfacher Nullsafe-Zugriff genügt dafür meist vollständig.


declare(strict_types=1);

final class ExpensiveResource
{
    public function __construct(public readonly string $id) {}
}

$resource = new ExpensiveResource('res-1');
$weakRef = WeakReference::create($resource);

// While $resource still holds a strong reference, get() returns the object
var_dump($weakRef->get()?->id); // string(5) "res-1"

// After releasing the only strong reference, the object becomes collectible
unset($resource);
gc_collect_cycles();

// The WeakReference itself never prevented garbage collection
var_dump($weakRef->get()); // NULL

3. WeakMap: Objekte als Schlüssel ohne Lebensdauerverlängerung

WeakMap baut auf demselben Prinzip auf, erlaubt aber, mehrere Objekte als Schlüssel mit beliebigen Werten zu verwalten, ähnlich wie ein assoziatives Array, jedoch mit Objekten statt Strings oder Integers als Schlüssel und ohne Einfluss auf deren Referenzzählung. Sobald ein als Schlüssel verwendetes Objekt keine andere starke Referenz mehr besitzt, entfernt PHP den entsprechenden Eintrag automatisch aus der WeakMap, ganz ohne manuelles Aufräumen durch den Entwickler.

Die Syntax orientiert sich bewusst an ArrayAccess, sodass sich eine WeakMap wie ein gewöhnliches Array mit eckigen Klammern verwenden lässt: $map[$objekt] = $wert und $map[$objekt] funktionieren direkt, ebenso isset() und unset(). Diese vertraute Syntax senkt die Einstiegshürde erheblich gegenüber der älteren SplObjectStorage-Klasse, die eine eigene, weniger intuitive Methodenschnittstelle mit attach() und detach() verwendet.


declare(strict_types=1);

/** @var WeakMap<object, array<string, mixed>> $metadata */
$metadata = new WeakMap();

$user = new stdClass();
$user->name = 'Alice';

// Attach metadata to the object without extending its lifetime
$metadata[$user] = ['lastAccess' => time(), 'requestCount' => 1];

echo $metadata[$user]['requestCount'], PHP_EOL; // 1

// Once $user has no other strong reference left, the entry disappears
// automatically — no manual cleanup, no leaked metadata array
unset($user);
gc_collect_cycles();

4. Einen Objekt-Cache mit WeakMap in der Praxis bauen

Ein praktisches Anwendungsbeispiel ist ein Cache für teure, abgeleitete Werte zu einem Objekt, etwa eine berechnete Prüfsumme oder eine serialisierte Repräsentation, die immer wieder für dasselbe Objekt benötigt wird, solange dieses existiert. Statt diesen Wert als Property in der Klasse selbst zu speichern, was die Klasse mit Cache-Logik verunreinigen würde, kapselt eine separate Cache-Klasse mit interner WeakMap diese Zuständigkeit sauber getrennt vom eigentlichen Domänenobjekt.

Der entscheidende Vorteil gegenüber einem klassischen Array-Cache: Die Cache-Klasse selbst kann beliebig lange existieren, etwa als Singleton während der gesamten Lebensdauer eines Worker-Prozesses, ohne dass jedes jemals gecachte Objekt dadurch dauerhaft im Speicher verbleibt. Sobald ein Domänenobjekt nicht mehr benötigt wird und keine andere Referenz mehr existiert, verschwindet auch sein Cache-Eintrag automatisch, ganz ohne explizite Invalidierungslogik.


declare(strict_types=1);

/**
 * Memoizes an expensive computation per object instance without
 * extending the lifetime of the cached objects.
 */
final class ChecksumCache
{
    /** @var WeakMap<object, string> */
    private WeakMap $cache;

    public function __construct()
    {
        $this->cache = new WeakMap();
    }

    /**
     * Compute and cache a checksum for the given object.
     *
     * @param object $target Domain object to compute a checksum for.
     * @return string The cached or freshly computed checksum.
     */
    public function checksumFor(object $target): string
    {
        return $this->cache[$target] ??= hash('sha256', serialize($target));
    }
}

$cache = new ChecksumCache(); // lives for the entire worker process lifetime

$order = new stdClass();
$order->total = 129.90;

echo $cache->checksumFor($order), PHP_EOL; // computed once, cached afterwards

5. Zusammenspiel mit Referenzzählung und Garbage Collection

PHPs primärer Speicherbereinigungsmechanismus ist die Referenzzählung: Sobald der Zähler eines Objekts auf null fällt, wird der Speicher sofort freigegeben, ohne auf einen periodischen Garbage-Collector-Durchlauf warten zu müssen. WeakMap-Einträge zählen dabei nicht zum Referenzzähler des Schlüsselobjekts, weshalb ein Objekt, dessen letzte starke Referenz entfernt wird, sofort freigegeben wird, auch wenn es noch als Schlüssel in einer WeakMap eingetragen war.

Der zyklische Garbage Collector kommt bei WeakMap nur in einem Sonderfall zusätzlich ins Spiel: wenn Objekte sich gegenseitig über normale, starke Referenzen zyklisch referenzieren und dadurch ihr Referenzzähler trotz fehlender externer Referenzen nie auf null fällt. Solche Zyklen werden weiterhin nur durch den periodischen Zyklus-Collector aufgelöst, unabhängig davon, ob eines der beteiligten Objekte zusätzlich als Schlüssel in einer WeakMap steht. Für die praktische Arbeit bedeutet das: WeakMap ersetzt nicht die Notwendigkeit, zyklische Referenzen im eigenen Domänenmodell zu vermeiden, sondern löst ausschließlich das Problem künstlich verlängerter Lebensdauer durch Cache-Strukturen.

6. Anwendungsfälle: Metadaten, Observer, Memoization

Neben Caches eignet sich WeakMap hervorragend für Metadaten-Zuordnungen, bei denen zusätzliche Informationen zu einem Objekt gespeichert werden sollen, ohne die Klasse selbst um zusätzliche Properties zu erweitern. Ein Beispiel: Ein Request-Tracking-System, das für jedes verarbeitete Domänenobjekt Zeitstempel und Verarbeitungsstatus vermerkt, ohne dass diese Informationen Teil der eigentlichen Domänenklasse werden müssen, was die Trennung von Fachlogik und Infrastruktur-Belangen deutlich sauberer hält.

Beim Observer-Pattern verhindert WeakMap ein subtiles, aber häufiges Problem: Ein Subject, das seine Observer in einer normalen Liste hält, verhindert deren Garbage Collection, selbst wenn der eigentliche Besitzer eines Observers längst keine Referenz mehr darauf hält. Wird die Observer-Liste stattdessen als WeakMap implementiert, können abgemeldete oder vergessene Observer automatisch verschwinden, ohne dass eine explizite Abmeldemethode aufgerufen werden muss, was in Event-lastigen Architekturen typische Speicherlecks verhindert.

Für Memoization, also das Zwischenspeichern von Funktionsergebnissen abhängig von einem Objektargument, ist WeakMap ebenfalls die natürliche Wahl gegenüber einem statischen Array, weil memoisierte Werte automatisch verschwinden, sobald das zugrundeliegende Objekt nicht mehr existiert, statt sich über die Laufzeit eines lang laufenden Prozesses unkontrolliert anzusammeln.

7. Grenzen von WeakMap: was nicht funktioniert

Eine wichtige Einschränkung: WeakMap akzeptiert ausschließlich Objekte als Schlüssel, keine Skalare wie Strings, Integers oder Arrays. Der Versuch, einen nicht-objekthaften Wert als Schlüssel zu verwenden, löst einen TypeError aus. Wer schwache Referenzen auf primitive Werte benötigt, muss diese zunächst in ein Wrapper-Objekt verpacken, was in der Praxis selten notwendig, aber gelegentlich der einzige Ausweg ist.

Ein zweiter wichtiger Punkt: WeakMap verhindert nicht, dass Werte innerhalb der Map selbst starke Referenzen auf andere Objekte halten. Wird als Wert ein Objekt gespeichert, das seinerseits eine starke Referenz zurück auf den Schlüssel hält, kann ein Referenzzyklus entstehen, der zwar vom zyklischen Garbage Collector letztlich aufgelöst wird, aber eben nicht sofort durch die schwache Referenzierung des Schlüssels selbst. WeakMap löst also gezielt das Problem der Schlüsselreferenz, nicht jedes denkbare Memory-Leak-Szenario in komplexen Objektgraphen.

8. Objektlebenszyklen sichtbar machen und debuggen

Um zu verifizieren, dass eine WeakMap tatsächlich wie erwartet funktioniert und keine Objekte länger als nötig am Leben hält, eignet sich ein einfacher Test mit gc_collect_cycles() gefolgt von einer Prüfung der WeakMap-Größe über count($map). Fällt die Anzahl der Einträge nach dem Entfernen aller externen Referenzen nicht auf den erwarteten Wert, deutet das auf eine versehentlich verbliebene starke Referenz irgendwo im Code hin, häufig in einer Closure, die das Objekt implizit über use erfasst hat.

Für die Analyse in komplexeren Anwendungen liefert memory_get_usage() vor und nach einer Verarbeitungsphase mit vielen erzeugten und wieder verworfenen Objekten ein verlässliches Signal, ob WeakMap-basierte Caches tatsächlich greifen. Ein stabiler Speicherverbrauch über viele Verarbeitungszyklen hinweg bestätigt, dass Objekte zuverlässig freigegeben werden, ein kontinuierlich wachsender Speicherverbrauch deutet dagegen auf eine versteckte starke Referenz hin, die trotz WeakMap-Einsatz an anderer Stelle im Code besteht.

9. WeakMap im Vergleich zu SplObjectStorage und Array

Ein direkter Vergleich zeigt, wann welche Struktur für objektbasierte Zuordnungen die richtige Wahl ist.

Struktur Verhindert GC des Schlüssels? Syntax Empfehlung
Array mit Objekt als Wert Ja, starke Referenz Gewöhnliche Array-Syntax Nur für kurzlebige Requests
SplObjectStorage Ja, starke Referenz attach() / detach() Wenn Lebensdauer bewusst verlängert werden soll
WeakMap Nein, schwache Referenz ArrayAccess, wie ein Array Caches und Metadaten in lang laufenden Prozessen
WeakReference Nein, schwache Referenz create() / get() Einzelnes Objekt beobachten

Der Vergleich macht deutlich: Sobald ein Objekt lediglich als Schlüssel für zusätzliche Informationen dienen soll, ohne dass diese Zuordnung dessen Lebensdauer beeinflussen darf, ist WeakMap die richtige Wahl. SplObjectStorage und gewöhnliche Arrays bleiben dort sinnvoll, wo eine bewusste Lebensdauerverlängerung tatsächlich gewünscht ist.

Mironsoft

PHP Speicheranalyse, Worker-Prozesse und speichersichere Cache-Architekturen

Memory Leaks in euren lang laufenden PHP-Prozessen finden?

Wir analysieren objektbasierte Caches, Observer-Strukturen und Metadaten-Zuordnungen in Worker-Prozessen und ersetzen künstlich lebensdauerverlängernde Strukturen durch WeakMap-basierte, speichersichere Alternativen.

Cache-Architektur-Review

Identifikation von Array- und SplObjectStorage-basierten Caches mit Leak-Risiko

WeakMap-Migration

Umbau bestehender Cache- und Observer-Strukturen auf schwache Referenzen

Speicher-Monitoring

Kontinuierliche Beobachtung von memory_get_usage() in produktiven Worker-Prozessen

10. Zusammenfassung

WeakMap und WeakReference lösen ein strukturelles Problem klassischer Objekt-Caches: Sie erlauben es, Objekte zu referenzieren, ohne deren Referenzzähler zu erhöhen, wodurch der Garbage Collector Objekte einsammeln kann, sobald keine andere starke Referenz mehr existiert. Für lang laufende Prozesse wie Worker mit Swoole oder RoadRunner ist das der entscheidende Unterschied zwischen einem stabilen Speicherverbrauch und einem schleichenden Memory Leak.

WeakMap eignet sich besonders für Caches, Metadaten-Zuordnungen, Observer-Strukturen und Memoization, während WeakReference für die Beobachtung einzelner Objektlebenszyklen gedacht ist. Wichtig bleibt, dass WeakMap keine Referenzzyklen zwischen normalen, starken Referenzen auflöst, sondern gezielt das Problem der künstlichen Lebensdauerverlängerung durch Schlüsselreferenzen in Cache-Strukturen adressiert.

WeakMap und WeakReference in PHP, das Wichtigste auf einen Blick

Keine Erhöhung des Referenzzählers

WeakMap-Schlüssel und WeakReference-Ziele werden nicht als starke Referenz gezählt.

Automatisches Aufräumen

Einträge verschwinden automatisch, sobald das Schlüsselobjekt keine andere Referenz mehr hat.

Vertraute Array-Syntax

WeakMap implementiert ArrayAccess und verhält sich syntaktisch wie ein gewöhnliches Array.

Nur für Objekte als Schlüssel

Skalare Werte müssen in ein Wrapper-Objekt verpackt werden, direkte Skalar-Schlüssel sind nicht erlaubt.

11. FAQ: WeakMap und WeakReference in PHP

1Was ist eine WeakMap?
Eine Struktur mit Objekten als Schlüsseln, die deren Referenzzähler nicht erhöht, Einträge verschwinden automatisch mit dem Objekt.
2Ab welcher Version gibt es WeakMap?
Seit PHP 8.0, WeakReference bereits seit PHP 7.4.
3Warum verursacht ein Array-Cache Leaks?
Arrays halten starke Referenzen, Objekte können solange der Cache lebt niemals eingesammelt werden.
4Kann ich Strings als Schlüssel nutzen?
Nein, nur Objekte sind als Schlüssel erlaubt, Skalare lösen einen TypeError aus.
5Löst WeakMap Referenzzyklen auf?
Nein, Zyklen zwischen starken Referenzen brauchen weiterhin den zyklischen Garbage Collector.
6Unterschied zu SplObjectStorage?
SplObjectStorage hält starke Referenzen, WeakMap schwache, das ist der zentrale Unterschied.
7Wofür eignet sich WeakReference?
Für die Beobachtung eines einzelnen Objekts ohne Beeinflussung seiner Lebensdauer.
8Wie prüfe ich das Verhalten?
gc_collect_cycles() gefolgt von count($map) zeigt, ob Einträge wie erwartet verschwinden.
9Lohnt sich WeakMap überall?
Besonders in lang laufenden Prozessen, in kurzen PHP-FPM-Requests ist der Effekt gering.
10Verhält sich WeakMap wie ein Array?
Ja, dank ArrayAccess funktionieren eckige Klammern, isset() und count() direkt.