Una variable en PHP se almacena en un contender llamado "zval". Un contenedor zval contiene, además del tipo de la variable y su valor, dos bits de información adicional. Al primero se le llama "is_ref" y contiene un boolean que indica si la variable es parte o no de con "conjunto de referencias". Con este bit, el motor de PHP sabe diferenciar entre variables normales y referencias. Puesto que PHP permite las 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, al que el navegador realizó la petición), además de uno por cada función o método.
Se crea un contenedor zval cuando se crea una 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 nuevo nombre de 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" por omisión contiene FALSE
dado que no se
ha creado ninguna referencia. "refcount" contiene 1 pues
sólo hay un símbolo que haga uso de este contenedor de variables. 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 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 otra, se incrementará refcount.
Ejemplo #3 Incremento del refcount de 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 variables está vinculado tanto por a como por b. PHP es lo suficiente inteligente para no copiar el contenedor de la variable cuando no es necesario. Los contenedores de variables se destruyen cuando el "refcount" se queda a cero. "refcount" se decrementa en uno cuando alguno de los símbolos que lo vinculan, abandona su ámbito (p.ej. cuando finaliza una función) o cuando se llama a unset(). El siguiente ejemplo muestra esto:
Ejemplo #4 Decremento del refcount de zval
<?php
$a = "nuevo string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
?>
El resultado del ejemplo sería:
a: (refcount=3, 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.
Las cosas se complican con tipos compuestos tales como arrays o objects. 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 Creando 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 )
O gráficamente
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ñadiendo 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' )
O gráficamente
A partir de la salida de Xdebug, vemos que tanto el antiguo como el nuevo elemento del array apuntan a un contenedor 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, pero puede comprobarse mostrando el puntero de memoria.
Eliminar un elemento del array es como eliminar un símbolo de un determinado á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 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 usaremos el operador de referencia, ya que sino PHP crearía una copia:
Ejemplo #8 Añadiendo al 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)=... )
O gráficamente
Puede verse que tanto la variable de tipo array (a) como el segundo elemento (1) apuntan ahora a un contenedor de variables 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 original. array.
Al igual que antes, al eliminar una variable se elimina el símbolo, y el contador de referencias del contenedor de variables 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 variables al que apuntan tanto $a como el elemento "1" se decrementa en uno, de "2" a "1". Se puede representar así:
Ejemplo #9 Eliminando $a
(refcount=1, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... )
O gráficamente
Pese a que ya no hay ningún símbolo en ningún ámbito que apunte a esta estructura, no se puede limpiar ya que el elemento "1" del array todavía apunta al mismo array. Al no haber ningún símbolo externo que apunte a él, no hay ninguna 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, pero antes de entonces, ocupará un valioso espacio en 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 al 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 sucede miles, o incluso millones, de estas fugas de memoria, lógicamente esto comenzaría a ser un problema. Es especialmente problemático en scripts de larga duración, tales como demonios donde en resumen nunca terminan las peticiones, o en un largo conjunto de pruebas unitarias. Esto último causó problemas al ejecutar las pruebas unitarias de la biblioteca de Componentes eZ. En algunos casos, pueden ser necesarios 2 GB de memoria, que quizás no los tenga el servidor de pruebas.