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
.