Speicherverwaltung und Garbage Collection in PHP verstehen
AI generated
<?php
8.4
PHP 8.4 · Speicherverwaltung · Garbage Collection · Performance
Speicherverwaltung und Garbage Collection in PHP verstehen
vom Zend Memory Manager bis zum Cycle Collector

PHP verwaltet Speicher scheinbar unsichtbar im Hintergrund, doch wer versteht, wie Reference Counting, der Zend Memory Manager und die Garbage Collection tatsächlich zusammenspielen, erkennt Speicherlecks in langlebigen Prozessen, bevor sie zum Produktionsproblem werden. Dieser Artikel erklärt die zval-Struktur, zirkuläre Referenzen, den Algorithmus des Cycle Collectors und die praktischen Werkzeuge zur Diagnose, für PHP-Entwickler, die tiefer als bis zu memory_limit denken wollen.

18 Min. Lesezeit Zend Memory Manager · zval · Cycle Collector · WeakMap PHP 8.1 - 8.4

1. Das PHP-Speichermodell: Zend Memory Manager und Heap-Segmente

Bevor man über Garbage Collection in PHP sprechen kann, muss man verstehen, dass PHP gar nicht direkt mit malloc und free arbeitet. Zwischen der Zend Engine und dem Betriebssystem sitzt der Zend Memory Manager (ZMM), der über emalloc(), efree() und erealloc() Speicher anfordert und freigibt. Der ZMM fordert vom Betriebssystem große, zusammenhängende Blöcke an, sogenannte Heap-Segmente (Chunks von typischerweise 2 MB), und teilt diese intern über eine größenklassenbasierte Freiliste weiter auf, ähnlich einem Slab-Allocator. Der Effekt: Die überwiegende Mehrheit der emalloc-Aufrufe erreicht niemals den Betriebssystem-Kernel, sondern wird vollständig innerhalb eines bereits reservierten Segments bedient.

Der Unterschied zum klassischen malloc/free ist fundamental: malloc ist ein generischer, prozessweiter Allocator ohne Konzept eines Request-Lebenszyklus, jede Anforderung kann potenziell einen Systemaufruf (brk oder mmap) auslösen. Der ZMM hingegen kennt in klassischen SAPIs wie PHP-FPM den Request als Einheit: Am Ende jedes Requests wird der komplette Heap in einem einzigen Schritt (zend_mm_shutdown) an das Betriebssystem zurückgegeben, unabhängig davon, ob innerhalb des Requests jede einzelne Variable korrekt freigegeben wurde. Das ist der Grund, warum viele Speicherprobleme in klassischem PHP historisch "unsichtbar" blieben. Für Strukturen, die über Requests hinweg leben müssen, etwa den Opcode-Cache oder persistente Datenbankverbindungen, existiert mit pemalloc() ein separater, persistenter Speicherbereich, der diesen Reset explizit umgeht.

Die Heap-Segmente sind das Substrat, auf dem Garbage Collection operiert, allerdings indirekt: Der Cycle Collector verwaltet nur die Erreichbarkeit von zval-Werten, nicht die rohen Speicherblöcke selbst. Erst wenn ein Refcount auf null fällt und efree() aufgerufen wird, gibt der ZMM den entsprechenden Slot innerhalb eines Segments zur Wiederverwendung frei. Fragmentierung innerhalb eines Segments kann also erst aufgelöst werden, nachdem die eigentliche Speicherverwaltung auf zval-Ebene ihre Arbeit erledigt hat, was die enge Kopplung zwischen Reference Counting und dem darunterliegenden Allocator verdeutlicht.

2. Reference Counting: die zval-Struktur im Detail

Jeder PHP-Wert wird intern als zval repräsentiert, eine getaggte Union aus einem Werteteil und einem Typ-Tag. Für refcounted Typen wie Arrays, Objekte, Strings (ab einer gewissen Länge) und Referenzen zeigt der Werteteil auf eine gemeinsame zend_refcounted-Kopfstruktur, die unter anderem das Feld refcount enthält. Bei einer Zuweisung wie $b = $a wird der eigentliche Wert nicht kopiert, sondern lediglich der refcount der zugrunde liegenden Struktur erhöht, beide Variablen zeigen danach auf denselben Speicherbereich. Bei Arrays greift zusätzlich Copy-on-Write: Erst eine tatsächliche Mutation von $b löst eine echte Kopie aus, solange der Refcount größer als eins ist. Objekte verhalten sich anders als Arrays, PHP arbeitet hier grundsätzlich mit Handles, jede Variable, die ein Objekt "enthält", hält einen Zeiger auf denselben zend_object, eine Zuweisung erhöht immer nur den Refcount, niemals wird das Objekt selbst kopiert.

Wird eine Variable mit unset() entfernt oder verlässt einen Scope, dekrementiert die Engine den Refcount der referenzierten Struktur. Erreicht dieser null, wird der Speicher sofort freigegeben, bei Objekten über den free_obj-Handler inklusive Aufruf des Destruktors, danach über efree() zurück an den Zend Memory Manager. Das folgende Beispiel macht diese Refcount-Änderungen mit debug_zval_refcount() sichtbar:


<?php
declare(strict_types=1);

class Payload
{
    public function __construct(public array $data) {}
}

$a = new Payload(['id' => 1]);
// Refcount of the zend_object is 1 (only $a points to it)
echo debug_zval_refcount($a), PHP_EOL; // 1

$b = $a; // No copy: refcount of the shared zend_object is incremented
echo debug_zval_refcount($a), PHP_EOL; // 2

unset($b); // Refcount decremented back to 1, object stays alive via $a
echo debug_zval_refcount($a), PHP_EOL; // 1

unset($a); // Refcount reaches 0: destructor runs, memory returned via efree()

Reference Counting allein erledigt bei den allermeisten PHP-Objekten die komplette Speicherverwaltung: Die meisten Objektgraphen in typischem Anwendungscode sind Bäume oder kurzlebige, funktionsgebundene Werte ohne Zyklen, der Refcount erreicht deterministisch null, sobald die letzte Referenz verschwindet, und der Speicher wird sofort freigegeben, ganz ohne einen nachgelagerten Sammel-Durchlauf. Genau deshalb ist die eigentliche Garbage Collection in PHP als Fallback-Mechanismus konzipiert und nicht als primärer Freigabepfad, sie greift nur in dem Sonderfall, in dem reines Refcounting strukturell versagt.

3. Zirkuläre Referenzen: das Grundproblem von reinem Refcounting

Der fundamentale Schwachpunkt von reinem Reference Counting zeigt sich, sobald zwei oder mehr Objekte sich gegenseitig referenzieren. Hält ein Objekt A eine Referenz auf Objekt B und B gleichzeitig eine Referenz zurück auf A, dann behalten beide Objekte auch dann einen Refcount von mindestens eins, wenn keine externe Variable mehr auf sie zeigt: Jedes hält das andere künstlich am Leben. Reines Refcounting kann diesen Zustand nicht erkennen, weil es lokal pro Objekt entscheidet und keinen globalen Blick auf die Erreichbarkeit von außen hat.

Solche Zyklen sind in der Praxis keine Ausnahme, sondern ein wiederkehrendes Muster: doppelt verkettete Listen, Eltern-Kind-Baumstrukturen mit Rückreferenz auf den Parent, oder Event-Listener als Closures, die $this einfangen, während das Subjekt selbst die Liste seiner Listener hält. Das folgende Beispiel demonstriert das Problem konkret:


<?php
declare(strict_types=1);

class Node
{
    public ?Node $parent = null;
    public ?Node $child = null;
}

function createLeakingCycle(): void
{
    $parent = new Node();
    $child = new Node();

    // Circular reference: parent points to child, child points back to parent
    $parent->child = $child;
    $child->parent = $parent;

    // Both objects still reference each other here, so unset() alone
    // never brings either refcount down to zero via reference counting.
}

// Each call leaves an unreachable but non-freed cycle behind
for ($i = 0; $i < 100_000; $i++) {
    createLeakingCycle();
}

echo memory_get_usage(), PHP_EOL; // grows steadily without the Garbage Collection cycle collector

Ohne einen zusätzlichen Mechanismus würde dieser Code in einem langlebigen Prozess kontinuierlich Speicher verbrauchen, obwohl die beiden Node-Objekte nach jedem Funktionsaufruf von außen komplett unerreichbar sind. Genau dieses Szenario, verkettete Strukturen und Closures mit gegenseitigen Referenzen, war der zentrale Auslöser dafür, dass PHP ab Version 5.3 einen echten Cycle Collector als Ergänzung zur reinen Speicherverwaltung per Refcounting eingeführt hat.

4. Der Zend Garbage Collector (Cycle Collector)

Der Zend Garbage Collector arbeitet mit einem sogenannten Root-Buffer: Immer wenn der Refcount eines potenziellen Zyklus-Kandidaten (Array, Objekt oder Referenz) dekrementiert wird, ohne dabei auf null zu fallen, trägt die Engine diesen Kandidaten in den Root-Buffer ein. Dieser Puffer hat eine feste Standardgröße von 10.000 Einträgen (GC_ROOT_BUFFER_MAX_ENTRIES). Ist der Puffer voll, löst die Engine automatisch einen vollständigen Sammel-Durchlauf aus, das ist der konkrete Auslöser, wann die automatische Garbage Collection tatsächlich einsetzt, unabhängig davon, wie viel Speicher der Prozess insgesamt verbraucht.

Der Algorithmus selbst basiert auf der synchronisierten Zyklen-Erkennung nach Bacon und Rajan und läuft in drei Phasen ab. In der Mark-Gray-Phase dekrementiert der Collector versuchsweise die Refcounts aller Referenzen innerhalb des zu prüfenden Teilgraphen, um zu simulieren, was passieren würde, wenn alle externen Referenzen entfernt würden. In der Scan-Phase unterscheidet er dann zwischen Knoten, die weiterhin einen positiven Refcount von außerhalb des Teilgraphen haben (als "schwarz" markiert, also tatsächlich erreichbar), und Knoten, deren Refcount auf null gefallen ist (als "weiß" markiert, also nur durch den Zyklus selbst am Leben gehalten). In der finalen Collect-Phase werden alle weißen Knoten gemeinsam als eine Einheit freigegeben.

Wichtig für das Verständnis der Performance-Charakteristik: Dieser Durchlauf ist synchron und blockierend, es handelt sich um eine "Stop-the-World"-Operation innerhalb des aktuellen Requests oder Prozesses, deren Dauer proportional zur Größe des durchsuchten Objektgraphen ist. Das unterscheidet PHPs Garbage Collection deutlich von generationellen oder inkrementellen Sammlern, wie man sie aus Sprachen mit dediziertem Laufzeitsystem kennt, und ist der Grund, warum ein großer Root-Buffer in speicherintensiven Anwendungen spürbare Latenzspitzen verursachen kann.

5. gc_collect_cycles(), gc_enable() und zend.enable_gc

PHP stellt drei zentrale Stellschrauben für die manuelle Steuerung der Garbage Collection bereit. gc_collect_cycles() erzwingt sofort einen vollständigen Sammel-Durchlauf über den aktuellen Root-Buffer, unabhängig davon, ob dieser bereits voll ist, und gibt die Anzahl der eingesammelten Zyklen zurück. gc_enable() und gc_disable() steuern, ob neue Kandidaten überhaupt in den Root-Buffer aufgenommen werden. Wichtig dabei: gc_disable() deaktiviert nicht das Reference Counting selbst, sondern nur die zusätzliche Buchführung für Zyklenerkennung, die bei jeder Refcount-Dekrementierung einen kleinen, aber messbaren Overhead verursacht. Die ini-Direktive zend.enable_gc legt den Startzustand fest, mit dem ein Prozess initialisiert wird.

Manuelle Steuerung wird vor allem in langlebigen CLI-Workern relevant, etwa in Queue-Konsumenten, die tausende Jobs ohne den klassischen Request-Grenzwert verarbeiten, bei dem der ZMM den kompletten Heap zurücksetzen würde. In latenzsensitiven Hot Loops kann es sinnvoll sein, die automatische Garbage Collection gezielt zu deaktivieren und stattdessen an definierten Checkpoints, etwa zwischen zwei Jobs statt mitten in einem, explizit gc_collect_cycles() aufzurufen. So bestimmt man selbst, wann die Stop-the-World-Pause auftritt, statt sie unvorhersehbar mitten in einer zeitkritischen Operation zu erleiden.


; php.ini: Garbage Collection tuning for long-running worker processes

; Enable the cycle collector at process startup (default: On)
zend.enable_gc = On

; Memory ceiling per process / worker (see section 8 for sizing guidance)
memory_limit = 256M

; Recommended for long-lived CLI workers (Swoole, RoadRunner, FrankenPHP):
; keep zend.enable_gc = On globally, but call gc_disable() / gc_collect_cycles()
; explicitly in the worker loop for deterministic pause placement.

6. WeakMap und WeakReference: Zyklen von vornherein vermeiden

WeakReference::create($objekt) erzeugt seit PHP 7.4 eine Referenz, die den Refcount des Zielobjekts nicht erhöht und dessen Lebensdauer nicht beeinflusst. Der Aufruf von get() liefert das Objekt zurück, solange es noch existiert, und null, sobald es an anderer Stelle bereits destruiert wurde. WeakMap, eingeführt in PHP 8.0, geht einen Schritt weiter und assoziiert beliebige Daten mit Objekten als Schlüssel, ohne diese Objekte am Leben zu halten. Wird der Schlüssel destruiert, entfernt PHP den zugehörigen Eintrag automatisch aus der WeakMap. Der entscheidende Vorteil gegenüber der klassischen Garbage Collection per Cycle Collector: Es entsteht gar keine starke zirkuläre Referenz, die überhaupt erst erkannt und aufgelöst werden müsste, das Problem wird strukturell vermieden statt nachträglich behoben.

Ein typischer Praxisfall ist eine Event-Listener-Registry im Observer-Pattern: Ein Subjekt hält eine Liste seiner Listener, während Listener als Closures oft $this des Subjekts einfangen, ein klassischer Zyklus-Kandidat. Mit einer WeakMap als Speicherstruktur für die Listener-Zuordnung hält die Registry das Subjekt nicht mehr künstlich am Leben, der Eintrag verschwindet automatisch, sobald das Subjekt anderweitig nicht mehr referenziert wird, ganz ohne dass der Cycle Collector jemals eingreifen muss:


<?php
declare(strict_types=1);

final class EventListenerRegistry
{
    /** @var WeakMap<object, array<callable>> */
    private WeakMap $listeners;

    public function __construct()
    {
        // Keys are held weakly: registering a subject here does not
        // increment its refcount and does not prevent it from being freed.
        $this->listeners = new WeakMap();
    }

    public function register(object $subject, callable $listener): void
    {
        $this->listeners[$subject] ??= [];
        $this->listeners[$subject][] = $listener;
    }

    public function notify(object $subject, mixed $payload): void
    {
        foreach ($this->listeners[$subject] ?? [] as $listener) {
            $listener($payload);
        }
    }
}

$registry = new EventListenerRegistry();
$subject = new class { public string $name = 'order.created'; };

// The closure captures $subject, and the registry stores it too,
// but as a WeakMap key, so no reference cycle is ever created.
$registry->register($subject, function (mixed $payload) use ($subject): void {
    echo "Handled {$subject->name}: {$payload}" . PHP_EOL;
});

unset($subject); // Freed immediately via reference counting, no cycle to collect

7. Memory Leaks in langlebigen Prozessen: Worker-Queues, Swoole, RoadRunner, FrankenPHP

Klassisches PHP unter PHP-FPM basiert auf einer impliziten Annahme: Am Ende jedes Requests wird der komplette Heap in einem Schritt verworfen, unabhängig davon, ob innerhalb des Requests jede Variable sauber freigegeben wurde. Selbst unentdeckte zirkuläre Referenzen oder in globalen Strukturen akkumulierte Daten "heilen sich selbst" bei jedem neuen Request. Diese Annahme gilt unter Swoole, RoadRunner und FrankenPHP im Worker-Modus nicht mehr: Der PHP-Prozess bzw. Worker lebt über tausende oder zehntausende Requests hinweg, jede Klassenkonstruktion passiert nur einmal beim Start des Workers, nicht mehr pro Request. Alles, was nicht explizit freigegeben wird oder aufgrund eines Zyklus nicht kollektierbar ist, akkumuliert über die gesamte Lebensdauer des Prozesses.

Konkrete Leck-Vektoren in diesem Kontext sind statische Properties und Singletons, die über Requests hinweg Daten anhäufen, globale Caches mit unbegrenzt wachsenden Arrays, nicht geschlossene Ressourcen wie Datenbankverbindungen oder Dateihandles in Dependency-Containern, die zwischen Requests nicht zurückgesetzt werden, sowie Event-Dispatcher, bei denen Listener registriert, aber nie wieder entfernt werden. Da Konstruktoren in Worker-Prozessen nur einmal laufen, muss Anwendungscode explizit dafür sorgen, dass Zustand zwischen Requests zurückgesetzt wird, sonst wächst der Speicherverbrauch schleichend über Stunden oder Tage, bis der Prozess neu gestartet werden muss, ein Verhalten, das bei klassischem request-basiertem PHP schlicht nicht existiert.

8. memory_limit dimensionieren und Monitoring mit memory_get_usage() und gc_status()

Die ini-Direktive memory_limit ist eine harte Obergrenze pro Prozess bzw. Worker, die vor unkontrolliert wachsenden Skripten schützen soll, sie ist aber nicht mit dem tatsächlichen Speicherverbrauch auf Betriebssystemebene identisch. Eine sinnvolle Dimensionierung basiert auf real gemessenen Spitzenwerten unter produktionsnaher Last, ermittelt über memory_get_peak_usage(true), wobei das Argument true den tatsächlich vom Zend Memory Manager beim Betriebssystem angeforderten Speicher liefert, inklusive interner Fragmentierung, nicht nur die von PHP-Werten belegten Bytes. Auf diesen Wert sollte man einen Sicherheitspuffer aufschlagen, und bei Worker-basierten SAPIs zusätzlich den prozessweiten RSS-Wert überwachen, da memory_limit dort nicht automatisch pro Request zurückgesetzt wird.

Für die Diagnose der eigentlichen Garbage Collection liefert gc_status() ein assoziatives Array mit Feldern wie runs, collected, threshold und vor allem roots, der aktuellen Anzahl an Einträgen im Root-Buffer. Wächst dieser Wert kontinuierlich, ohne durch automatische oder manuelle Sammel-Durchläufe wieder zu sinken, deutet das entweder auf eine schnellere Erzeugung von Zyklen als deren Kollektion hin oder darauf, dass gc_disable() versehentlich aktiv geblieben ist:


<?php
declare(strict_types=1);

/**
 * Prints a compact memory / GC snapshot, useful as a checkpoint
 * inside a long-running worker loop (Swoole, RoadRunner, FrankenPHP).
 */
function memorySnapshot(string $label): void
{
    $status = gc_status();

    printf(
        "[%s] used=%.2fMB peak=%.2fMB gc_runs=%d collected=%d roots=%d%s",
        $label,
        memory_get_usage(true) / 1_048_576,
        memory_get_peak_usage(true) / 1_048_576,
        $status['runs'],
        $status['collected'],
        $status['roots'],
        PHP_EOL
    );
}

memorySnapshot('startup');

for ($job = 0; $job < 1000; $job++) {
    processJob($job); // application-specific work happens here

    if ($job % 100 === 0) {
        memorySnapshot("after job {$job}");
    }
}

// Force a collection pass at a safe checkpoint and compare roots before/after
$collected = gc_collect_cycles();
memorySnapshot("after gc_collect_cycles ({$collected} cycles freed)");

9. Praxis-Debugging: ein Speicherleck systematisch eingrenzen

Die systematische Eingrenzung eines Speicherlecks beginnt mit regelmäßigen Snapshots an festen Checkpoints, etwa nach jedem hundertsten verarbeiteten Job, bestehend aus memory_get_usage(true) und gc_status()['roots']. Trägt man diese Werte über die Zeit auf, ergibt sich ein charakteristisches Muster: Ein sägezahnartiger oder stabiler Verlauf, bei dem der Speicherverbrauch periodisch wieder sinkt, deutet auf gesundes Verhalten hin, die Garbage Collection räumt Zyklen zuverlässig auf. Ein Verlauf, der auch nach einem erzwungenen gc_collect_cycles() monoton weiter ansteigt, ist dagegen ein starkes Indiz für ein echtes Leck: Es handelt sich dann nicht um unaufgelöste Zyklen, sondern um Referenzen, die tatsächlich nie freigegeben werden, etwa weil ein globaler Cache oder eine statische Property unbegrenzt wächst.

Zur weiteren Eingrenzung verdächtiger Objektgraphen hilft Reflection: Da PHP ohne zusätzliche Erweiterung keine eingebaute Funktion bietet, um alle aktuell lebenden Objektinstanzen aufzulisten, inspiziert man gezielt statische Properties und globale Registries über ReflectionClass::getStaticProperties(), um zu prüfen, welche Objekte dort über die Zeit angesammelt werden. Als indirektes Signal dient auch der Rückgabewert von gc_collect_cycles(): Ein plötzlicher Sprung in der Anzahl kollektierter Zyklen an einer bestimmten Stelle im Code deutet auf den verantwortlichen Codepfad hin. Bewährt hat sich zudem eine Bisektions-Strategie: Teile der Request- oder Job-Verarbeitung gezielt deaktivieren und die Speicherkurve erneut messen, um das Leck auf ein konkretes Subsystem einzugrenzen.

Für tiefere Analysen bieten sich spezialisierte Werkzeuge wie das Xdebug-Profiling oder Blackfire an, die vollständige Objektgraphen und Allokationspfade sichtbar machen. In produktiven Worker-Prozessen hat es sich zusätzlich bewährt, einen internen Debug-Endpunkt bereitzustellen, der gc_status() und memory_get_usage() auf Anfrage ausgibt, um Regressionen direkt nach einem Deployment zu erkennen, bevor der Speicherverbrauch kritische Schwellenwerte erreicht.

Reference Counting, Cycle Collector und WeakMap im Vergleich

Die drei Mechanismen der Speicherverwaltung in PHP lösen unterschiedliche Teilprobleme und unterscheiden sich deutlich in Overhead und Zeitpunkt des Eingreifens. Die folgende Tabelle stellt sie gegenüber:

Mechanismus Löst zirkuläre Referenzen Performance-Overhead Wann greift es
Reference Counting Nein Sehr gering, pro Zuweisung/unset Sofort, bei jeder Refcount-Änderung auf 0
Cycle Collector (Garbage Collection) Ja, nachträglich Spürbar bei großen Graphen (Stop-the-World) Automatisch bei vollem Root-Buffer oder manuell via gc_collect_cycles()
WeakMap / WeakReference Ja, präventiv Minimal, kein Refcount-Increment auf den Schlüssel Von Anfang an, keine Kollektion nötig

In der Praxis ergänzen sich die drei Mechanismen: Reference Counting bleibt der primäre Freigabepfad für die überwiegende Mehrheit aller Werte, der Cycle Collector fängt die verbleibenden Zyklen ab, und WeakMap/WeakReference vermeiden Zyklen bereits im Entwurf, wo sie architektonisch vorhersehbar sind, etwa bei Registries, Caches und Observer-Patterns.

10. Zusammenfassung

Garbage Collection in PHP ist kein einzelner Mechanismus, sondern ein Zusammenspiel aus drei Ebenen: Der Zend Memory Manager verwaltet Heap-Segmente unterhalb von emalloc/efree und gibt sie am Requestende in klassischen SAPIs gebündelt frei. Reference Counting über die zval-Struktur erledigt die eigentliche Speicherverwaltung für die überwiegende Mehrheit aller Werte sofort und deterministisch. Der Cycle Collector als eigentliche Garbage Collection im engeren Sinn springt nur dort ein, wo Refcounting strukturell versagt, bei zirkulären Referenzen, erkennbar an einem wachsenden Root-Buffer und steuerbar über gc_collect_cycles(), gc_enable()/gc_disable() und zend.enable_gc.

WeakMap und WeakReference lösen das Problem bereits präventiv, indem sie Zyklen gar nicht erst entstehen lassen, ideal für Registries, Caches und Observer-Patterns. In langlebigen Worker-Prozessen unter Swoole, RoadRunner oder FrankenPHP gilt die klassische Annahme "Speicher wird am Requestende komplett freigegeben" nicht mehr, hier sind Monitoring über memory_get_usage(), memory_get_peak_usage() und gc_status() sowie eine bewusste, checkpoint-basierte Steuerung der Garbage Collection keine Kür, sondern Voraussetzung für stabilen Dauerbetrieb.

Garbage Collection in PHP: Das Wichtigste auf einen Blick

Reference Counting

zval-basiert, dekrementiert sofort bei unset() oder Scope-Ende. Erledigt die Speicherverwaltung für die meisten Werte ohne Garbage Collection.

Cycle Collector

Root-Buffer mit 10.000 Slots, synchronisierte Zyklenerkennung nach Bacon & Rajan. Greift automatisch bei vollem Puffer oder via gc_collect_cycles().

WeakMap & WeakReference

Vermeiden zirkuläre Referenzen strukturell, ideal für Event-Registries und Observer-Patterns ohne Memory Leak.

Monitoring

memory_get_usage(true), memory_get_peak_usage(true) und gc_status()['roots'] als Checkpoints in langlebigen Workern.

11. FAQ: Garbage Collection in PHP

1Reference Counting vs. Garbage Collection?
Reference Counting zählt Referenzen und gibt sofort frei, wenn der Zähler 0 erreicht. Garbage Collection im PHP-Sinn ist der Cycle Collector, der nur Zyklen löst.
2Wann springt der Garbage Collector automatisch an?
Sobald der Root-Buffer die Standardgröße von 10.000 Kandidaten erreicht, löst die Engine automatisch einen Durchlauf aus.
3Was macht gc_collect_cycles()?
Erzwingt sofort einen vollständigen Sammel-Durchlauf und gibt die Anzahl freigegebener Zyklen zurück. Nützlich für Checkpoints in Workern.
4Wann gc_disable() nutzen?
In latenzsensitiven Hot Loops, um Stop-the-World-Pausen selbst per gc_collect_cycles() an sicheren Checkpoints zu platzieren.
5WeakMap vs. WeakReference?
WeakReference hält eine schwache Referenz auf ein einzelnes Objekt. WeakMap assoziiert Daten mit mehreren Objekten als Schlüssel und entfernt Einträge automatisch.
6Löst WeakMap alle Leck-Probleme?
Nein, nur zirkuläre Referenzen an der Einsatzstelle. Wachsende Caches oder statische Properties löst WeakMap nicht.
7Warum sind Swoole/RoadRunner-Worker anders?
PHP-FPM verwirft den Heap pro Request. Worker-Runtimes halten den Prozess über tausende Requests am Leben, nicht zurückgesetzter Zustand akkumuliert.
8memory_limit richtig dimensionieren?
Auf Basis von memory_get_peak_usage(true) unter realer Last plus Sicherheitspuffer, bei Workern zusätzlich RSS beobachten.
9Was sagt gc_status()['roots']?
Aktuelle Anzahl Kandidaten im Root-Buffer. Kontinuierliches Wachstum trotz gc_collect_cycles() deutet auf ein echtes Leck hin.
10Verwaltet der Cycle Collector auch int/string?
Nein, nur Arrays, Objekte und Referenzen, da nur diese Typen zirkuläre Strukturen bilden können.

Mironsoft

PHP-Performance, Speicherdiagnose und langlebige Worker-Prozesse

Speicherlecks in PHP-Workern zuverlässig aufspüren?

Wir analysieren langlebige PHP-Prozesse auf zirkuläre Referenzen, unkontrollierte Root-Buffer und wachsende Caches, und bringen Garbage Collection, memory_limit und Monitoring in ein belastbares Verhältnis für euren Produktionsbetrieb.

Speicheranalyse

gc_status() und memory_get_usage() Monitoring in bestehende Worker integrieren

Refactoring

Zirkuläre Referenzen durch WeakMap und WeakReference strukturell auflösen

Worker-Tuning

memory_limit, zend.enable_gc und Checkpoint-Strategien für Swoole, RoadRunner, FrankenPHP