PHP 8.3.4 Released!

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.

Algorithmus für die Garbage Collection

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.

add a note

User Contributed Notes 3 notes

up
18
Dallas
5 years ago
After testing, breaking up memory intensive code into a separate function allows the garbage collection to work.

For example the original code was like:-
while(true){
//do memory intensive code
}

can be turned into something like:-
function intensive($parameters){
//do memory intensive code
}

while(true){
intensive($parameters);
}
up
12
Yousha dot A at Hotmail dot com
8 years ago
Memory leak: meaning you keep a reference to it thus preventing the GC from collecting it.
up
9
Yousha dot A at Hotmail dot com
7 years ago
── Unused Objects ─── ─ In use Objects
↓ ↓ ↓
_____________________________________
|□□□□□□□□□□□□□□□□□|██■■■■■■■■■■■■■■■■|
|□□□□□□□□□□□□□□□□□|██■■■■■■■■■■■■■■■■|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
▲ ▲
Unreferenced Referenced
Objects Objects

█ Memory leak
To Top