In diesem Artikel werden die Inhalte im Zusammenhang mit Garbage Collection und Speicherverwaltung im Entwicklungsprozess von PHP beschrieben.
Referenzzählung
In PHP 5.2 und früheren Versionen verwendet die Garbage Collection von PHP den Algorithmus „Referenzzählung“. Grundkenntnisse der Referenzzählung
Grundkenntnisse der ReferenzzählungPHP-Variablen werden im Variablencontainer „zval“ (Datenstruktur) gespeichert. Das Attribut „zval“ enthält die folgenden Informationen:
Der Datentyp der aktuellen Variablen;- Der Wert der aktuellen Variablen;
- Der boolesche Typbezeichner is_ref, der verwendet wird, um zu identifizieren, ob die Variable als Referenz übergeben wird;
- Der Refcount-Bezeichner, der auf die Anzahl der Variablen im Variablencontainer „zval“ verweist (Das heißt, wie oft auf dieses zval verwiesen wurde. Achten Sie hier darauf. Die Referenz bezieht sich nicht auf die Übergabe von Werten. Bitte achten Sie auf die Unterscheidung.)
- Wenn einer Variablen ein Wert zugewiesen wird, wird ein entsprechender „zavl“-Variablencontainer generiert. [Empfohlenes Lernen:
]Anzeigen der Variablen-Zval-Containerinformationen
Um die „zval“-Containerinformationen der Variablen anzuzeigen (d. h. is_ref und refcount der Variablen anzuzeigen), können Sie
verwenden xdebug_debug_zval()Funktion des XDebug-Debugging-Tools. Informationen zur Installation des XDebug-Erweiterungs-Plug-Ins finden Sie in diesem Tutorial (https://github.com/huliuqing/phpnotes/issues/58). Informationen zur Verwendung von XDebug finden Sie im offiziellen Dokument (https: //xdebug.org/docs/).
Angenommen, wir haben das XDebug-Tool erfolgreich installiert und können nun Variablen debuggen.
Sehen Sie sich die ZVAL-Informationen gewöhnlicher Variablen an- Wenn unsere PHP-Anweisung einfach eine Variable zuweist, ist der is_ref-Identifikatorwert 0 und der Refcount-Wert ist 1; wenn diese Variable als Wert einer anderen Variablen zugewiesen wird, dann Erhöhen Sie die Wenn die Variable zerstört (nicht gesetzt) wird, wird der Refcount-Zähler des zval-Variablencontainers entsprechend um 1 subtrahiert.
Bitte sehen Sie sich das folgende Beispiel an:
<?php // 变量赋值时,refcount 值等于 1 $name = 'liugongzi'; xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9) // $name 作为值赋值给另一个变量, refcount 值增加 1 $copy = $name; xdebug_debug_zval('name'); // (refcount=2, is_ref=0)string 'liugongzi' (length=9) // 销毁变量,refcount 值减掉 1 unset($copy); xdebug_debug_zval('name'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
- Copy On Write (COW), einfach beschrieben als: Wenn einer Variablen durch Zuweisung ein Wert zugewiesen wird, wird kein neuer Speicher zum Speichern des neuen zugewiesen Der von der Variablen gespeicherte Wert teilt sich einfach den Speicher über einen Zähler. Nur wenn sich der Wert, auf den einer der Verweise auf die Variable zeigt, ändert, wird neuer Speicherplatz zugewiesen, um den Wertinhalt zu speichern und so die Speichernutzung zu reduzieren. - TPIP Copy-on-Write
Durch die Zval-Informationen der vorherigen einfachen Variablen wissen wir, dass$copy
und $name den Zval-Variablencontainer (Speicher) gemeinsam nutzen und dann refcount zur Angabe verwenden Wie viele Variablen werden derzeit von dieser zval-Verwendung verwendet? Sehen Sie sich ein Beispiel an:
<?php $name = 'liugongzi'; xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9) $copy = $name; xdebug_debug_zval('name'); // name: (refcount=2, is_ref=0)string 'liugongzi' (length=9) // 将新的值赋值给变量 $copy $copy = 'liugongzi handsome'; xdebug_debug_zval('name'); // name: (refcount=1, is_ref=0)string 'liugongzi' (length=9) xdebug_debug_zval('copy'); // copy: (refcount=1, is_ref=0)='liugongzi handsome'
Ist Ihnen aufgefallen, dass die Refcount-Werte von Name und Kopie 1 werden, wenn der Wert
liugongzi gorgeousder Variablen $copy zugewiesen wird? Vorgänge werden ausgeführt:
Trennen Sie die $-Kopie von der $Name-Refcount (d. h. kopieren Sie);- Subtrahieren Sie die $name-Refcount-Zahl (neu zuweisen)
- Hier ist nur eine kurze Einführung in das Thema „Kopieren beim Schreiben“. Interessierte Freunde können die Referenzmaterialien am Ende des Artikels für tiefergehende Recherchen lesen.
- Die Regeln für die „Referenzzählung“ der Referenzübergabe (&) sind dieselben wie bei gewöhnlichen Zuweisungsanweisungen, mit der Ausnahme, dass der Wert von
- is_ref ist
zeigt an, dass es sich bei der Variablen um einen Referenztyp handelt. Sehen wir uns nun ein Beispiel für die Übergabe als Referenz an: <?php $age = 'liugongzi';
xdebug_debug_zval('age'); // (refcount=1, is_ref=0)string 'liugongzi' (length=9)
$copy = &$age;
xdebug_debug_zval('age'); // (refcount=2, is_ref=1)string 'liugongzi' (length=9)
unset($copy);
xdebug_debug_zval('age'); // (refcount=1, is_ref=1)string 'liugongzi' (length=9)
- Unterscheidet sich von Skalartypen (Ganzzahl, Gleitkomma, Boolescher Wert usw.), Arrays und Objekten. Die Regeln für Typ- Die konforme Referenzzählung ist etwas komplizierter.
- Zur besseren Erklärung schauen wir uns zunächst das Referenzzählbeispiel eines Arrays an:
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=2) // 'meaning' => (refcount=1, is_ref=0)string 'life' (length=4) // 'number' => (refcount=1, is_ref=0)int 42
Das obige Referenzzähldiagramm sieht wie folgt aus:
Aus dem Bild sehen wir, dass die Referenzzählregeln zusammengesetzter Typen lauten Im Grunde dasselbe wie beim Zählen von Skalaren. Die Regeln sind die gleichen. Im gegebenen Beispiel erstellt PHP drei zval-Variablencontainer, einen zum Speichern des Arrays selbst und zwei zum Speichern der Elemente im Array.Wenn Sie dem Array ein vorhandenes Element hinzufügen, wird dessen Referenzzähler-Refcount um 1 erhöht.
$a = array( 'meaning' => 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); $a['life'] = $a['meaning']; xdebug_debug_zval( 'a' ); // a: // (refcount=1, is_ref=0) // array (size=3) // 'meaning' => (refcount=2, is_ref=0)string 'life' (length=4) // 'number' => (refcount=0, is_ref=0)int 42 // 'life' => (refcount=2, is_ref=0)string 'life' (length=4)
Das grobe Diagramm sieht wie folgt aus:
- 内存泄露
虽然,复合类型的引用计数规则同标量类型大致相同,但是如果引用的值为变量自身(即循环应用),在处理不当时,就有可能会造成内存泄露的问题。
让我们来看看下面这个对数组进行引用传值的示例:
<?php // @link http://php.net/manual/zh/function.memory-get-usage.php#96280 function convert($size) { $unit=array('b','kb','mb','gb','tb','pb'); return @round($size/pow(1024,($i=floor(log($size,1024)))),2).' '.$unit[$i]; } // 注意:有用的地方从这里开始 $memory = memory_get_usage(); $a = array( 'one' ); // 引用自身(循环引用) $a[] =&$a; xdebug_debug_zval( 'a' ); var_dump(convert(memory_get_usage() - $memory)); // 296 b unset($a); // 删除变量 $a,由于 $a 中的元素引用了自身(循环引用)最终导致 $a 所使用的内存无法被回收 var_dump(convert(memory_get_usage() - $memory)); // 568 b
从内存占用结果上看,虽然我们执行了 unset($a) 方法来销毁 $a 数组,但内存并没有被回收,整个处理过程的示意图如下:
可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:
尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。 - 摘自 官方文档 Cleanup Problems
简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。
引用计数系统的同步周期回收
由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:
- 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。
- 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。
- 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。
下图(来自 PHP 手册),展示了新的回收算法执行过程:
引用计数系统的同步周期回收过程
- 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);
- 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);
- 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);
- 删除所有 refcount 为 0 的可能根(步骤 D)。
整个过程为:
采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。
优化后的引用计数算法优势
- 将内存泄露控制在阀值内,这个由缓存区实现,达到缓冲区大小执行新一轮垃圾回收;
- 提升了垃圾回收性能,不是每次 refcount 减 1 都执行回收处理,而是等到根缓冲区满时才开始执行垃圾回收。
你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。
PHP 7 的内存管理
PHP 5 中 zval 实现上的主要问题:
- zval reserviert immer Speicher nur aus dem Heap ;
- zval speichert immer Referenzzählungs- und Recyclinginformationen , auch für Ganzzahldaten (bool/null), die solche Informationen möglicherweise nicht erfordern;
- Bei der Verwendung von Objekten oder Ressourcen Direkte Verweise führen zu einer Doppelzählung.
- Einige indirekte Zugriffe erfordern eine bessere Handhabung. Beispielsweise werden beim Zugriff auf in Variablen gespeicherte Objekte jetzt indirekt vier Zeiger verwendet (die Länge der Zeigerkette beträgt vier).
- Direktes Zählen bedeutet, dass Werte nur zwischen Zvals geteilt werden können. Dies funktioniert nicht, wenn Sie eine Zeichenfolge zwischen einem ZVAL und einem Hashtable-Schlüssel teilen möchten (es sei denn, der Hashtable-Schlüssel ist auch ein ZVAL).
Anpassungen an der zval-Datenstrukturimplementierung in PHP 7:
Die grundlegendste Änderung besteht darin, dass der von zval benötigte Speicher nicht mehr getrennt vom Heap zugewiesen wird und zval keine Referenzzähler mehr speichert. Der Referenzzähler komplexer Datentypen (wie Zeichenfolgen, Arrays und Objekte) wird selbst gespeichert. - Auszug aus Interne Wertdarstellung in PHP 7 - Teil 1 【Übersetzung】Vorteile dieser Implementierung:
- Einfache Datentypen müssen keinen Speicher separat zuweisen und müssen nicht gezählt werden
- Es wird keinen weiteren geben Doppelzählung. In einem Objekt ist nur die im Objekt selbst gespeicherte Anzahl gültig.
- Da die Anzahl jetzt durch den Wert selbst gespeichert wird (PHP verfügt über einen zval-Variablencontainerspeicher), kann sie mit Daten in Nicht-zval-Strukturen geteilt werden, z zwischen zval und Hashtable-Schlüsselzeit;
- Die Anzahl der für den indirekten Zugriff erforderlichen Zeiger wird reduziert.
Weitere Einzelheiten zur PHP 7-zval-Implementierung und Speicheroptimierung finden Sie im Artikel „Ausführliches Verständnis von PHP7-Kernel-zval und interner Wertdarstellung in PHP 7 – Übersetzung Teil 1“. (https://www.npopov.com/2015/05/05/Internal-value-representation-in-PHP-7-part-1.html)