Bases sur le comptage des références

Une variable PHP est stockée en interne dans un conteneur appelé "zval". Un conteneur zval contient, outre le type de la variable et sa valeur, deux informations supplémentaires. La première se nomme "is_ref" et est une valeur booléenne qui indique si une variable fait partie d'une référence ou non. Grâce à cette information, le moteur de PHP sait différencier les variables normales des références. Comme PHP autorise le programmeur à utiliser des références, au moyen de l'opérateur &, un conteneur zval possède aussi un mécanisme de comptage des références afin d'optimiser l'utilisation de la mémoire. Cette seconde information, appelée "refcount", contient le nombre de variables (aussi appelées symboles) qui pointent vers ce conteneur zval. Tous les symboles sont stockés dans une table de symboles, et il y a une table par espace de visibilité (scope). Il y a un espace global pour le script principal (celui appelé par exemple via le navigateur) et un espace par fonction ou méthode.

Un conteneur zval est créé lorsqu'une nouvelle variable est créée avec une valeur constante, comme par exemple :

Exemple #1 Création d'un nouveau conteneur zval

<?php
$a 
"new string";
?>

Dans ce cas, le nouveau symbole a est créé dans le scope global, et un nouveau conteneur est créé avec comme type string et comme valeur new string. Le bit "is_ref" est mis par défaut à FALSE car aucune référence n'a été créée par le programmeur. Le compteur de références "refcount" est mis à 1 car il n'y a qu'un seul symbole qui utilise ce conteneur. Notez que si "refcount" vaut 1, "is_ref" vaut toujours FALSE. Si vous avez installé » Xdebug, vous pouvez afficher cette information en appelant xdebug_debug_zval().

Exemple #2 Affichage des informations zval

<?php
$a 
"new string";
xdebug_debug_zval('a');
?>

L'exemple ci-dessus va afficher :

a: (refcount=1, is_ref=0)='new string'

Assigner cette variable à un autre symbole va incrémenter le refcount.

Exemple #3 Incrémentation du refcount d'une zval

<?php
$a 
"new string";
$b $a;
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher :

a: (refcount=2, is_ref=0)='new string'

Le refcount vaut 2 ici, car le même conteneur est lié à la fois à a et à b. PHP est suffisamment intelligent pour ne pas dupliquer le conteneur lorsque ce n'est pas nécessaire. Les conteneurs sont détruits lorsque leur "refcount" atteint zéro. Le "refcount" est décrémenté de un lorsque n'importe quel symbole lié à un conteneur sort du scope (ex. : lorsque la fonction se termine) ou lorsqu'un symbole est déalloué (ex. : par l'appel de unset()). L'exemple qui suit le démontre :

Exemple #4 Décrémentation du refcount d'une zval

<?php
$a 
"new string";
$c $b $a;
xdebug_debug_zval'a' );
$b 42;
xdebug_debug_zval'a' );
unset( 
$c );
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher :

a: (refcount=3, is_ref=0)='new string'
a: (refcount=2, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'

Si, maintenant, nous appelons unset($a);, le conteneur zval, incluant le type et la valeur, va être supprimé de la mémoire.

Types composés

Les choses se compliquent dans le cas de types composés comme array et object. A la différence des valeurs scalaires, les array et object stockent leurs propriétés dans une table de symboles qui leur est propre. Ceci signifie que l'exemple qui suit crée trois conteneurs zval :

Exemple #5 Création d'une zval array

<?php
$a 
= array( 'meaning' => 'life''number' => 42 );
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher quelque chose de similaire à :

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

Ou graphiquement

Zvals d'un tableau simple

Les trois conteneurs zval sont : a, meaning, et number. Les mêmes règles s'appliquent pour l'incrémentation et la décrémentation des "refcounts". Ci-après, nous ajoutons un autre élément au tableau, et nous renseignons sa valeur avec le contenu d'un élément déjà existant du tableau :

Exemple #6 Ajout d'un élément déja existant au tableau

<?php
$a 
= array( 'meaning' => 'life''number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher quelque chose de similaire à :

a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

Ou graphiquement

Zvals pour un tableau simple avec une référence

La sortie Xdebug que nous voyons indique que l'ancien et le nouvel élément du tableau pointent maintenant tous deux vers un conteneur zval dont le "refcount" vaut 2. Même si la sortie XDebug montre deux conteneurs zval avec comme valeur 'life', ils sont les mêmes. La fonction xdebug_debug_zval() ne montre pas cela, mais vous pourriez le voir en affichant aussi le pointeur de mémoire.

Supprimer un élément du tableau est assimilable à la suppression d'un symbole depuis un espace. Ce faisant, le "refcount" du conteneur vers lequel l'élément du tableau pointe est décrémenté. Une fois encore, s'il atteint zéro, le conteneur zval est supprimé de la mémoire. Voici un exemple qui le démontre :

Exemple #7 Suppression d'un élément de tableau

<?php
$a 
= array( 'meaning' => 'life''number' => 42 );
$a['life'] = $a['meaning'];
unset( 
$a['meaning'], $a['number'] );
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher quelque chose de similaire à :

a: (refcount=1, is_ref=0)=array (
   'life' => (refcount=1, is_ref=0)='life'
)

Maintenant, les choses deviennent intéressantes si nous ajoutons le tableau comme élément de lui-même. Nous faisons cela dans l'exemple qui suit, en utilisant un opérateur de référence pour éviter que PHP ne crée une copie :

Exemple #8 Ajout du tableau comme référence à lui-même en tant qu'élement

<?php
$a 
= array( 'one' );
$a[] =& $a;
xdebug_debug_zval'a' );
?>

L'exemple ci-dessus va afficher quelque chose de similaire à :

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

Ou graphiquement

Zvals dans un tableau avec référence circulaire

Vous pouvez voir que la variable tableau (a) tout comme le second élément (1) pointent désormais vers un conteneur dont le "refcount" vaut 2. Les "..." sur l'affichage indiquent une récursion, qui, dans ce cas, signifie que le "..." pointe sur le tableau lui-même.

Comme précédemment, supprimer une variable supprime son symbole, et le refcount du conteneur sur lequel il pointaint est décrémenté. Donc, si nous supprimons la variable $a après avoir exécuté le code ci-dessus, le compteur de références du conteneur sur lequel pointent $a et l'élément "1" sera décrémenté de un, passant de "2" à "1". Ceci peut être représenté par :

Exemple #9 Suppression de $a

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

Ou graphiquement

Zvals après suppression du tableau contenant une référence circulaire, fuite mémoire

Problèmes de nettoyage

Bien qu'il n'y ait plus aucun symbole dans l'espace de variables courant qui pointe vers cette structure, elle ne peut être nettoyée, car l'élément "1" du tableau pointe toujours vers ce même tableau. Comme il n'y a plus aucun symbole externe pointant vers cette structure, l'utilisateur ne peut pas la nettoyer manuellement ; il y a donc une fuite de mémoire. Heureusement, PHP va détruire cette structure à la fin de la requête, mais avant cette étape, la mémoire n'est pas libérée. Cette situation se produit souvent si vous implémentez un algorithme d'analyse ou d'autres idées où vous avez un enfant qui pointe vers son parent. La même chose peut bien entendu se produire avec les objets, et c'est même plus probable, puisqu'ils sont toujours implicitement utilisés par référence.

Ceci peut ne pas être gênant si cela n'arrive qu'une ou deux fois, mais s'il y a des des milliers, ou même des millions, de ces fuites mémoires, alors cela risque évidemment de devenir un problème important. C'est particulièrement problématique pour les scripts qui durent longtemps, comme les démons pour lesquels la requête ne termine pour ainsi dire jamais, ou encore dans de grosses suites de tests unitaires. Ce dernier cas a été rencontré en lançant les tests unitaires du composant Template de la bibliothèque eZ Components. Dans certains cas, la suite de tests nécessitait plus de 2Go de mémoire, que le serveur le test n'avait pas vraiment à disposition.