Recolección de basura
PHP Manual

Introducción al contador de referencias

Una variable en PHP se almacena en un contenedor llamado "zval". Un contenedor zval contiene, además del tipo de la variable y su valor, dos bits adicionales de información. Al primero se le llama "is_ref" y contiene un valor booleano que indica si la variable es parte o no de un "conjunto de referencias". Con este bit, el motor de PHP sabe diferenciar entre variables normales y referencias. Puesto que PHP permite referencias definidas por el usuario, tal y como se crean con el operador &, un contenedor zval tiene también un mecanismo contador de referencias para optimizar el uso de memoria. Esta segunda pieza adicional de información, llamada "refcount", contiene el número de variables (también llamadas símbolos) que apuntan a este contenedor zval. Todos los símbolos se almacenan en una tabla de símbolos, de las cuales hay una por cada ámbito. Hay un ámbito para el script principal (es decir, el solicitado por el navegador), además de uno por cada función o método.

Se crea un contenedor zval al crear una nueva variable con un valor constante, como por ejemplo:

Ejemplo #1 Creación de un nuevo contenedor zval

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

En este caso, el nombre del nuevo símbolo, a, se crea en el ámbito actual y se crea un nuevo contenedor de variable con el tipo string y el valor nuevo string. El bit "is_ref" se establece por omisión a FALSE dado que no se ha creado ninguna referencia en el espacio del usuario. "refcount" se establece a 1, pues solo hay un símbolo que haga uso de este contenedor de variable. Tenga en cuenta que si "refcount" es 1, "is_ref" siempre valdrá FALSE. Si tiene » Xdebug instalado, puede mostrar esta información llamando a xdebug_debug_zval().

Ejemplo #2 Mostrar información de zval

<?php
xdebug_debug_zval
('a');
?>

El resultado del ejemplo sería:

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

Al asignar esta variable a otro nombre de variable, se incrementará refcount.

Ejemplo #3 Incremento de refcount de un zval

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

El resultado del ejemplo sería:

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

Aquí, refcount vale 2, pues el mismo contenedor de variable está vinculado tanto por a como por b. PHP es lo suficiente inteligente para no copiar el contenedor de variable en sí cuando no es necesario. Los contenedores de variables se destruyen cuando "refcount" llega a cero. "refcount" se decrementa en uno cuando alguno de los símbolos vinculados al contenedor de variable abandona su ámbito (p.ej. cuando finaliza una función) o cuando un símbolo es desasignado (p. ej., llamando a unset()). El siguiente ejemplo muestra esto:

Ejemplo #4 Decremento de refcount de zval

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

El resultado del ejemplo sería:

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

Si ahora llamáramos a unset($a);, el contenedor de variable, incluyendo tanto el tipo como el valor, se eliminarían de la memoría.

Tipos compuestos

Las cosas se complican con tipos compuestos tales como arrays y object. En lugar de un valor de tipo scalar, los arrays y objects almacenan sus propiedades en su propia tabla de símbolos. Esto significa que el siguiente ejemplo crea tres contenedores zval:

Ejemplo #5 Crear un zval de tipo array

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

El resultado del ejemplo sería algo similar a:

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

Gráficamente

Los zval de un array simple

Los tres contenedores zval son: a, meaning, y number. Se aplican reglas similares a la hora de incrementar y decrementar "refcounts". Abajo, añadimos otro elemento al array, y fijamos su valor al contenido de un elemento ya existente:

Ejemplo #6 Añadir un elemento existente a un array

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

El resultado del ejemplo sería algo similar a:

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'
)

Gráficamente

Los zval de un array simple con una referencia

A partir de la salida de Xdebug, vemos que tanto el antiguo como el nuevo elemento del array apuntan a un contenedor zval cuyo "refcount" vale 2. Pese a que Xdebug muestra dos contenedores zval con valor 'life', son el mismo. La función xdebug_debug_zval() no muestra esto, aunque podria comprobarse mostrando también el puntero de memoria.

Eliminar un elemento del array es como eliminar un símbolo de un ámbito. Al hacerlo, el "refcount" del contenedor al que apunta el elemento del array se decrementa. De nuevo, cuando "refcount" alcanza cero, el contenedor de la variable se elimina de la memoria. Un ejemplo que muestra esto:

Ejemplo #7 Eliminar un elemento de un array

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

El resultado del ejemplo sería algo similar a:

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

Ahora, las cosas se vuelven interesantes si añadimos al propio array como elemento del array, como veremos en el siguiente ejemplo, en el que también usaremos el operador de referencia, ya que de lo contrario PHP crearía una copia:

Ejemplo #8 Añadir el propio array como elemento de sí mismo

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

El resultado del ejemplo sería algo similar a:

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

Gráficamente

Los zval para un array que contiene una referencia circular

Puede verse que tanto la variable de tipo array (a) como el segundo elemento (1) apuntan ahora a un contenedor de variable que tiene un "refcount" de 2. Los "..." mostrados arriba indican que hay una referencia cíclica, lo cual, por supuesto, significa que en este caso los "..." apuntan al array original.

Al igual que antes, al eliminar una variable se elimina el símbolo y el contador de referencias del contenedor de variable al que apunte se decrementa en uno. De modo que, si eliminamos la variable $a después de ejecutar el código anterior, el contador de referencias del contenedor de variable al que apuntan tanto $a como el elemento "1" se decrementa en uno, de "2" a "1". Se puede representar así:

Ejemplo #9 Eliminar $a

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

Gráficamente

Los zval después de eliminar un array con referencia circular mostrando la fuga de memoria

Problemas de limpieza

Pese a que ya no hay ningún símbolo en ningún ámbito que apunte a esta estructura, esta no se puede limpiar ya que el elemento "1" del array todavía apunta al mismo array. Al no haber un símbolo externo que apunte a él, no hay forma por la que el usuario pueda eliminar esta estructura; por tanto tenemos una fuga de memoria. Afortunadamente, PHP limpiará esta estructura de datos al finalizar la petición, aunque hasta entonces esté ocupando un valioso espacio de memoria. Esta situación ocurre a menudo si se está implementando un algoritmo de análisis o en otras situaciones en las que un nodo hijo apunte de nuevo a un elemento "padre". Por supuesto, esta situación también puede suceder con objetos, donde es más frecuente que ocurra, ya que los objetos siempre se usan implícitamente por referencia.

Esto no debería ser un problema si sólo ocurre una o dos veces, pero si estas fugas de memoria suceden miles, o incluso millones de veces, lógicamente esto comenzaría a ser un problema. Es especialmente problemático en scripts de larga duración, tales como demonios donde básicamente nunca terminan las peticiones, o en un gran conjunto de pruebas unitarias. Esto último causó problemas al ejecutar las pruebas unitarias del componente Template de la biblioteca eZ Componentes. En algunos casos, podrían ser necesarios 2 GB de memoria que quizás no los tenga el servidor de pruebas.


Recolección de basura
PHP Manual