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
xdebug_debug_zval('a');
?>
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
?>
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
<?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.

LoadingChargement en cours