Maison  >  Article  >  développement back-end  >  Analyse détaillée du garbage collection et de la gestion de la mémoire en PHP

Analyse détaillée du garbage collection et de la gestion de la mémoire en PHP

不言
不言original
2018-08-15 09:40:551288parcourir

Cet article vous apporte une analyse détaillée du garbage collection et de la gestion de la mémoire en PHP. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il vous sera utile.

Comptage de références

Dans PHP 5.2 et les versions précédentes, le garbage collection de PHP utilise l'algorithme de comptage de références.

Connaissance de base du comptage de références

Les variables PHP sont stockées dans le conteneur de variables "zval" (structure de données). 🎜>

  • Le type de données de la variable actuelle

  • La valeur de la variable actuelle

  • est utilisé pour identifier si la variable est une référence. L'identifiant de type booléen is_ref transmis

  • pointe vers l'identifiant refcount du nombre de variables dans le conteneur de variables "zval" ; le nombre de fois que ce zval a été référencé. Notez que la référence ici ne fait pas référence à une valeur de réussite, faites attention à la distinction).

Lorsqu'une variable reçoit une valeur, un conteneur de variable "zavl" correspondant sera généré.

Afficher les informations du conteneur zval de la variable

Pour afficher les informations du conteneur "zval" de la variable (c'est-à-dire afficher l'is_ref et le refcount de la variable), vous peut utiliser l'outil de débogage XDebug

xdebug_debug_zval() fonction.

Pour savoir comment installer le plug-in d'extension XDebug, vous pouvez consulter ce tutoriel. Pour savoir comment utiliser XDebug, veuillez lire la documentation officielle.

Supposons que nous ayons installé avec succès l'outil XDebug et que nous puissions maintenant déboguer les variables.

  • Afficher les informations zval des variables ordinaires

Si notre instruction PHP n'est qu'une simple affectation de variables, la valeur de l'identifiant is_ref est 0, et le refcount La valeur est 1 ; si cette variable est affectée comme valeur à une autre variable, le nombre de refcount du conteneur de variables zval est augmenté de la même manière, lorsque la variable est détruite (non définie), le "refcount" est soustrait de 1 ; par conséquent.

Voir l'exemple suivant :

<?php // 变量赋值时,refcount 值等于 1
$name = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

// $name 作为值赋值给另一个变量, refcount 值增加 1
$copy = $name;
xdebug_debug_zval(&#39;name&#39;); // (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)

// 销毁变量,refcount 值减掉 1
unset($copy);
xdebug_debug_zval(&#39;name&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

Copier en écriture

Copier en écriture (Copier Sur Écrivez : COW), une description simple est la suivante : si une valeur est affectée à une variable par affectation, une nouvelle mémoire ne sera pas allouée pour stocker la valeur enregistrée par la nouvelle variable, mais la mémoire sera simplement partagée via un compteur, avec seulement une référence pointant vers elle Lorsque la valeur d'une variable change, un nouvel espace est alloué pour enregistrer le contenu de la valeur afin de réduire l'utilisation de la mémoire. - Copie sur écriture TPIP

D'après les informations zval précédentes des variables simples, nous savons que $copy et $name partagent le conteneur de variables zval (mémoire), puis utilisons refcount pour indiquer combien de variables utilisent actuellement ce zval.

Regardez un exemple :

<?php $name = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

$copy = $name;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=2, is_ref=0)string &#39;liugongzi&#39; (length=9)

// 将新的值赋值给变量 $copy
$copy = &#39;liugongzi handsome&#39;;
xdebug_debug_zval(&#39;name&#39;); // name: (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)
xdebug_debug_zval(&#39;copy&#39;); // copy: (refcount=1, is_ref=0)=&#39;liugongzi handsome&#39;
Remarquez que lorsque la valeur liugongzi beau est affectée à la variable $copy, les valeurs de refcount ​​​​de name et copy deviennent 1. Dans ce cas, les opérations suivantes se produisent au cours du processus :

  • Séparez la copie $ du zval (esclave interne) du nom $ (c'est-à-dire la copie

  • Soustraire 1 du refcount de $name;
  • Modifier le zval de $copy (réaffecter et modifier le refcount);
  • Voici juste une brève introduction à « copier en écrivant ». Les amis intéressés peuvent lire les documents de référence donnés à la fin de l'article pour une recherche plus approfondie.

    Afficher les informations zval de la variable passée par référence
  • Les règles de "comptage de références" du passage de référence par valeur (&) sont les mêmes que celles des instructions d'affectation ordinaires, sauf que la valeur de l'identifiant
is_ref

est 1 indiquant que la variable est un type de référence passé par valeur. Regardons maintenant un exemple de passage par référence :

<?php $age = &#39;liugongzi&#39;;
xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=0)string &#39;liugongzi&#39; (length=9)

$copy = &$age;
xdebug_debug_zval(&#39;age&#39;); // (refcount=2, is_ref=1)string &#39;liugongzi&#39; (length=9)

unset($copy);
xdebug_debug_zval(&#39;age&#39;); // (refcount=1, is_ref=1)string &#39;liugongzi&#39; (length=9)
    Comptage de références de types composites
  • Avec Différents types scalaires (entier, virgule flottante, booléen, etc.), les règles de comptage de références pour les types tels que les tableaux et les objets sont légèrement plus compliquées.

Pour une meilleure explication, regardons d'abord l'exemple de comptage de références d'un tableau :

Le schéma de comptage de références ci-dessus est le suivant :
$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

Analyse détaillée du garbage collection et de la gestion de la mémoire en PHPÀ partir de la figure, nous constatons que les règles de comptage de références des types composites sont fondamentalement les mêmes que les règles de comptage des scalaires. Pour l'exemple donné, PHP créera 3 conteneurs de variables zval, un. pour le stockage Le tableau lui-même et les deux autres sont utilisés pour stocker les éléments du tableau.

Lors de l'ajout d'un élément existant au tableau, son compteur de référence refcount sera augmenté de 1.

Le schéma approximatif est le suivant :
$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)

Analyse détaillée du garbage collection et de la gestion de la mémoire en PHP

    Fuite de mémoire
  • Bien que les règles de comptage de références des types composites soient à peu près les mêmes que celles des types scalaires, si la valeur de la référence est la variable elle-même (c'est-à-dire une application cyclique), cela peut provoquer des fuites de mémoire si elle n’est pas géré correctement.

Regardons l'exemple suivant de passage d'un tableau par référence :

<?php // @link http://php.net/manual/zh/function.memory-get-usage.php#96280
function convert($size)
{
    $unit=array(&#39;b&#39;,&#39;kb&#39;,&#39;mb&#39;,&#39;gb&#39;,&#39;tb&#39;,&#39;pb&#39;);
    return @round($size/pow(1024,($i=floor(log($size,1024)))),2).&#39; &#39;.$unit[$i];
}

// 注意:有用的地方从这里开始
$memory = memory_get_usage();

$a = array( &#39;one&#39; );

// 引用自身(循环引用)
$a[] =&$a;

xdebug_debug_zval( &#39;a&#39; );

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 数组,但内存并没有被回收,整个处理过程的示意图如下:

Analyse détaillée du garbage collection et de la gestion de la mémoire en PHP

可以看到对于这块内存,再也没有符合表(变量)指向了,所以 PHP 无法完成内存回收,官方给出的解释如下:

尽管不再有某个作用域中的任何符号指向这个结构 (就是变量容器),由于数组元素 “1” 仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。庆幸的是,php 将在脚本执行结束时清除这个数据结构,但是在 php 清除之前,将耗费不少内存。如果你要实现分析算法,或者要做其他像一个子元素指向它的父元素这样的事情,这种情况就会经常发生。当然,同样的情况也会发生在对象上,实际上对象更有可能出现这种情况,因为对象总是隐式的被引用。

简单来说就是「引用计数」算法无法检测并释放循环引用所使用的内存,最终导致内存泄露。

引用计数系统的同步周期回收

由于引用计数算法存在无法回收循环应用导致的内存泄露问题,在 PHP 5.3 之后对内存回收的实现做了优化,通过采用 引用计数系统的同步周期回收 算法实现内存管理。引用计数系统的同步周期回收算法是一个改良版本的引用计数算法,它在引用基础上做出了如下几个方面的增强:

  • 引入了可能根(possible root)的概念:通过引用计数相关学习,我们知道如果一个变量(zval)被引用,要么是被全局符号表中的符号引用(即变量),要么被复杂类型(如数组)的 zval 中的符号(数组的元素)引用,那么这个 zval 变量容器就是「可能根」。

  • 引入根缓冲区(root buffer)的概念:根缓冲区用于存放所有「可能根」,它是固定大小的,默认可存 10000 个可能根,如需修改可以通过修改 PHP 源码文件 Zend/zend_gc.c 中的常量 GC_ROOT_BUFFER_MAX_ENTRIES,再重新编译。

  • 回收周期:当缓冲区满时,对缓冲区中的所有可能根进行垃圾回收处理。

下图(来自 PHP 手册),展示了新的回收算法执行过程:

Analyse détaillée du garbage collection et de la gestion de la mémoire en PHP

引用计数系统的同步周期回收过程

  1. 缓冲区(紫色框部分,称为疑似垃圾),存储所有可能根(步骤 A);

  2. 采用深度优先算法遍历「根缓冲区」中所有的「可能根(即 zval 遍历容器)」,并对每个 zval 的 refcount 减 1,为了避免遍历时对同一个 zval 多次减 1(因为不同的根可能遍历到同一个 zval)将这个 zvel 标记为「已减」(步骤 B);

  3. 再次采用深度优先遍历算法遍历「可能根 zval」。当 zval 的 refcount 值不为 0 时,对其加 1,否则保持为 0。并请已遍历的 zval 变量容器标记为「已恢复」(即步骤 B 的逆运算)。那些 zval 的 refcount 值为 0 (蓝色框标记)的就是应该被回收的变量(步骤 C);

  4. 删除所有 refcount 为 0 的可能根(步骤 D)。

整个过程为:

采用深度优先算法执行:默认删除 > 模拟恢复 > 执行删除 达到内存回收的目的。

优化后的引用计数算法优势

  • 将内存泄露控制在阀值内,这个由缓存区实现,达到缓冲区大小执行新一轮垃圾回收;

  • 提升了垃圾回收性能,不是每次 refcount 减 1 都执行回收处理,而是等到根缓冲区满时才开始执行垃圾回收。

你可以从 PHP 手册 的回收周期 了解更多,也可以阅读文末给出的参考资料。

PHP 7 的内存管理

PHP 5 中 zval 实现上的主要问题:

  • zval 总是单独 从堆中分配内存;

  • zval 总是存储引用计数和循环回收 的信息,即使是整型(bool / null)这种可能并不需要此类信息的数据;

  • 在使用对象或者资源时,直接引用会导致两次计数;

  • Certains accès indirects nécessitent une meilleure façon de les gérer. Par exemple, quatre pointeurs sont désormais utilisés indirectement pour accéder aux objets stockés dans des variables (la longueur de la chaîne de pointeurs est de quatre)

  • Le comptage direct signifie que les valeurs ne peuvent être partagées qu'entre ; zvals. Cela ne fonctionne pas si vous souhaitez partager une chaîne entre un zval et une clé de table de hachage (sauf si la clé de table de hachage est également un zval).

Ajustement de l'implémentation de la structure de données zval dans PHP 7 :

Le changement le plus fondamental est que la mémoire requise par zval n'est plus allouée à partir du tas seul, et n'est plus alloué par zval stocke le nombre de références.
Le nombre de références des types de données complexes (tels que les chaînes, les tableaux et les objets) est stocké par lui-même.

Avantages de cette implémentation :

  • Les types de données simples ne nécessitent pas d'allocation de mémoire séparée et ne nécessitent pas de comptage

  • Plus de double comptage. Dans un objet, seul le nombre stocké dans l'objet lui-même est valide

  • Puisque le nombre est maintenant stocké par la valeur elle-même (PHP a un stockage de conteneur variable zval), il peut également être combiné avec des structures non-zval Partage de données, par exemple entre zval et la clé de table de hachage

  • Le nombre de pointeurs requis pour l'accès indirect est réduit.

Articles connexes recommandés :

Gestion de la mémoire et garbage collection des scripts PHP - Article personnel Sifu

Apprenez de moi le mécanisme de collecte des ordures et la gestion de la mémoire des compétences javascript_javascript

Collecte des ordures Une brève explication du mécanisme de collecte des ordures PHP

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn