Sammeln zyklischer Referenzen
In der Vergangenheit konnten referenzzählende Speichermechanismen, wie der
von PHP verwendete, zyklische Referenzspeicherlecks nicht beheben. Seit
Version 5.3.0 implementiert PHP jedoch den synchronen Algorithmus aus der
Abhandlung
» Concurrent Cycle Collection in Reference Counted Systems,
die dieses Problem behandelt.
Eine vollständige Erklärung der Funktionsweise des Algorithmus würde den
Rahmen dieses Abschnitts etwas sprengen, aber die Grundlagen werden hier
erläutert. Zunächst müssen ein paar Grundregeln festgelegt werden. Wenn
ein Referenzzähler erhöht wird, ist er noch in Gebrauch und daher kein
Garbage. Wenn der Referenzzähler verringert wird und Null erreicht, kann
das Zval freigegeben werden. Das bedeutet, dass problematische Zyklen nur
dann erzeugt werden können, wenn ein refcount-Argument auf einen Wert
ungleich Null verringert wird. Damit kann festgestellt werden, welche
Teile in einem problematischen Zyklus Garbage sind, indem geprüft wird, ob
es möglich ist, ihren Referenzzähler um eins zu verringern, und dann
geprüft wird, welche der Zvals einen Referenzzähler von Null haben.
Um zu vermeiden, dass die Überprüfung von problematischen Zyklen bei jeder
möglichen Verringerung eines Referenzzählers aufgerufen werden muss, legt
der Algorithmus stattdessen alle möglichen Wurzeln (Zvals) in den
"Wurzelpuffer" (und markiert sie "lila"). Er sorgt auch dafür, dass jede
mögliche Wurzel nur einmal im Puffer landet. Erst wenn der Wurzelpuffer
voll ist, beginnt der Sammelmechanismus für alle verschiedenen Zvals
darin. Siehe Schritt A in der Abbildung oben.
In Schritt B führt der Algorithmus bei allen möglichen Wurzeln eine
Tiefensuche durch, um die Referenzzähler jedes gefundenen Zvals um eins zu
verringern. Dabei wird darauf geachtet, dass der Referenzzähler desselben
Zvals nicht zweimal verringert wird (indem sie als "grau" markiert
werden). In Schritt C führt der Algorithmus erneut eine Tiefensuche bei
jedem Wurzelknoten aus, um den Referenzzähler jedes Zvals erneut zu
überprüfen. Stellt er fest, dass der Referenzzähler Null ist, wird das
Zval als "weiß" markiert (in der Abbildung blau). Ist er größer als Null,
wird die Verringerung des Refcount um eins mit einer Tiefensuche von
diesem Punkt an rückgängig gemacht, und die Zvals werden wieder "schwarz"
markiert. Im letzten Schritt (D) geht der Algorithmus über den
Wurzelpuffer und entfernt die Zval-Wurzeln von dort, wobei er gleichzeitig
überprüft, welche Zvals im vorherigen Schritt als "weiß" markiert wurden.
Jedes Zval, das als "weiß" markiert wurde, wird freigegeben.
Nachdem nun ein grundlegendes Verständnis für die Funktionsweise des
Algorithmus vorhanden ist, wird nun erläutert, wie dieser in PHP
integriert ist. Standardmäßig ist der Garbage Collector von PHP aktiviert,
aber es gibt eine php.ini-Einstellung, mit der dies geändert werden
kann: zend.enable_gc.
Wenn der Garbage Collector aktiviert ist, wird der oben beschriebene
Algorithmus zum Finden von Zyklen immer dann ausgeführt, wenn der
Wurzelpuffer voll ist. Der Wurzelpuffer hat eine feste Größe von 10.000
möglichen Wurzeln (obwohl man dies ändern kann, indem man im PHP-Quellcode
in Zend/zend_gc.c
die Konstante
GC_THRESHOLD_DEFAULT
ändert und PHP neu kompiliert).
Wenn der Garbage Collector deaktiviert ist, wird der Algorithmus zum
Finden von Zyklen nie ausgeführt. Allerdings werden mögliche Wurzeln immer
im Wurzelpuffer aufgezeichnet, unabhängig davon, ob der Mechanismus der
Garbage Collection mit dieser Konfigurationseinstellung aktiviert wurde.
Wenn der Wurzelpuffer bei deaktiviertem Mechanismus der Garbage Collection
vollständig mit möglichen Wurzeln gefüllt ist, werden weitere mögliche
Wurzeln einfach nicht aufgezeichnet. Die möglichen Wurzeln, die nicht
aufgezeichnet werden, werden vom Algorithmus nie analysiert. Wären sie
Teil eines zirkulären Referenzzyklus, würden sie nie bereinigt werden und
ein Speicherleck verursachen.
Dass mögliche Wurzeln aufgezeichnet werden, auch wenn der Mechanismus
deaktiviert wurde, liegt daran, dass es schneller ist, mögliche Wurzeln
aufzuzeichnen, als jedes Mal, wenn eine mögliche Wurzel gefunden wurde, zu
prüfen, ob der Mechanismus aktiviert ist. Der Mechanismus der Garbage
Collection und Analyse selbst kann jedoch sehr viel Zeit in Anspruch
nehmen.
Neben der Änderung der Konfigurationseinstellung
zend.enable_gc ist es auch
möglich, den Mechanismus der Garbage Collection zu aktivieren oder zu
deaktivieren, indem man gc_enable() bzw.
gc_disable() aufruft. Diese Funktionen haben den
gleichen Effekt wie die Konfigurationseinstellung, mit der der Mechanismus
aktiviert oder deaktiviert wird. Es ist auch möglich, das Sammeln von
Zyklen zu erzwingen, selbst wenn der Wurzelpuffer noch nicht voll ist.
Dazu kann die Funktion gc_collect_cycles() verwendet
werden. Diese Funktion ermittelt, wieviele Zyklen durch den Algorithmus
gesammelt wurden.
Der Grund für die Möglichkeit, den Mechanismus zu aktivieren und zu
deaktivieren und die Sammlung von Zyklen selbst einzuleiten, besteht darin,
dass einige Teile einer Anwendung sehr zeitkritisch sein könnten. In
diesen Fällen ist es vielleicht nicht erwünscht, dass der Mechanismus der
Garbage Collection ausgelöst wird. Wenn die Garbage Collection für
bestimmte Teile einer Anwendung ausgeschaltet wird, besteht natürlich die
Gefahr, dass Speicherlecks entstehen, weil einige mögliche Wurzeln nicht
in den begrenzten Wurzelpuffer passen. Daher ist es wahrscheinlich ratsam,
gc_collect_cycles() kurz vor dem Aufruf von
gc_disable() aufzurufen, um den Speicher freizugeben,
der durch eventuell bereits im Wurzelpuffer aufgezeichnete Wurzeln
verloren gegangen sein könnte. Dadurch bleibt ein leerer Puffer übrig,
sodass mehr Platz für die Speicherung möglicher Wurzeln vorhanden ist,
während der Mechanismus zum Sammeln von Zyklen deaktiviert ist.