Ramasse-miettes (Garbage Collection)
PHP Manual

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 unités d'information additionnelles. La première se nomme "is_ref" et 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ée lorsqu'une nouvelle variable est créee avec une valeur constante, par exemple:

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

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

Dans ce cas, le nouveau symbole a est crée dans le scope global, et un nouveau conteneur est crée 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éee par le programmeur. Le bit "refcount" iest 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');
?>

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é à a et b à la fois. PHP est suffisament 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 une unité lorsque n'importe quel symbole lié à un conteneur est détruit du scope (e.g. lorsque la fonction se termine) ou lorsque unset() est appelée sur un symbole. L'exemple qui suit le démontre:

Exemple #4 Decrémentation du refcount d'une zval

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

L'exemple ci-dessus va afficher :

a: (refcount=3, 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 détruit de la mémoire.

Types composés

Les choses se compliquent dans le cas des types composés comme array et object. A la différence des valeurs scalaires scalar, 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 Creation 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 élément au tableau et nous affectons son contenu à une valeur déja existante dans le 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 les anciens et nouveaux éléments du tableau pointent maintenant vers un conteneur zval dont le "refcount" vaut 2. Même si la sortie XDebug montre 2 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 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 atteind 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 ça dans l'exemple qui suit avec un opérateur de référence, sinon PHP va 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 et représentent donc le tableau lui-même.

Comme avant, supprimer une variable supprime son symbole et le refcount est décrémenté. Donc si nous supprimons la variable $a après son effectation, le refcount du conteneur sur lequel pointe $a et l'élément "1" sera décrémenté de une unité, de "2" vers "1". Ceci est 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 du tableau "1" pointe toujours vers ce même tableau. Comme il n'y a plus de symbole externe pointant vers cette structure, l'utilisateur ne peut plus la nettoyer manuellement, il y a donc fuite 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 lorsque qu'un enfant pointe vers son parent. Notamment avec les objets qui sont utilisés par référence implicitement.

Ceci ne devrait pas être un problème si ça arrive une ou deux fois, mais s'il y a des centaines ou des milliers de situations similaires dans un même programme, alors cela peut devenir un problème important. Ceci en particulier pour les scripts démons dans lesquels la requête ne termine jamais, ou encore dans une grosse suite de tests unitaires. Ce dernier cas a été rencontré en lançant les tests du composant Template de la bibliothèque eZ Components. Dans certains cas, la suite de tests nécessitait 2Go de mémoire, ce que le serveur ne pouvait alors fournir.


Ramasse-miettes (Garbage Collection)
PHP Manual