PHP Mémoire - noelno/dovelei GitHub Wiki

Une variable PHP est stockée en interne dans un conteneur "zval".

Ce conteneur contient :

  • le nom de la variable
  • le type de la variable
  • un booléen is_ref qui indique s’il s’agit d’une valeur primitive (false) ou d’une référence (true)
  • un entier ref_count, qui compte le nombre de variables / symboles pointant sur ce conteneur (par défaut un : lui-même).

Toutes les variables sont stockées dans la table des symboles correspondant à leur scope (scope global ou scope d’une méthode ou d’une fonction en particulier).

Avec Xdebug on a accès au détail de zval pour chaque variable grâce à xdebug_debug_zval('maVariable'); (il faut cependant que la variable soit visible dans le scope où est appelé cette fonction, sinon elle retournera null.

Par exemple une variable $nomVar valant valeurVar sera représentée ainsi :

nomVar: (refcount=1, is_ref=0)=valeurVar

Quand on assigne $b à $a, PHP fait en sorte que les deux variables se partagent le même conteneur zval pour économiser de la mémoire. Le ref_count du zval passe alors à 2. Dès qu’une des variables pointant sur le zval n’est plus dans le scope ou est unset, ref_count est décrémenté.

En ce qui concerne les types composés, par exemple les valeurs stockées dans des tableaux, elles ont chacune un conteneur zval. Quand on assigne $a['cle1'] à $a['cle2'], PHP fait en sorte que les deux valeurs se partagent le même conteneur zval pour économiser de la mémoire.

Quand le ref_count d’un zval passe à 0, il est automatiquement supprimé.

Quand on ajoute comme élément d’un tableau $a une référence à lui-même en indice 1 :

a: (refcount=2, is_ref=1)=array (
    0 => (refcount=1, is_ref=0)='one',
    1 => (refcount=2, is_ref=1)=...
)

(Les … indiquent une récursion.)

Le problème est que si l’on fait un unset sur $a, le ref_count du zval de $a va passer à 1 et non à 0, ce qui fait que le zval de $a ne sera pas supprimé de la mémoire alors que $a n’existera plus. Le résultat est une fuite de mémoire (memory leak).

Il faudrait donc d’abord faire un unset de $a[1] avant d’unset $a.

(refcount=1, is_ref=1)=array (
    0 => (refcount=1, is_ref=0)='one',
    1 => (refcount=1, is_ref=1)=...
)

Un script à tester pour observer ce comportement : http://paul-m-jones.com/archives/262

<?php
class Foo {
    function __construct()
    {
        $this->bar = new Bar($this);
    }
}

class Bar {
    function __construct($foo = null)
    {
        $this->foo = $foo;
    }
}

while (true) {
    $foo = new Foo();
    unset($foo);
    echo number_format(memory_get_usage()) . "n";
}
?>

Dans ce script je crée un objet $foo contenant un objet $bar prenant en paramètre son parent $foo. Bien que $foo soit supprimé, son zval reste en mémoire, d’où la valeur de la mémoire utilisée qui augmente sans arrêt. Il faudrait pour empêcher la fuite faire un unset sur $bar avant de supprimer $foo.