Recolección de basura
PHP Manual

Recolección de referencias cíclicas

Tradicionalmente, los mecanismos que contabilizan las referencias en memoria, tal como el que empleaba PHP anteriormente, fallaban al abordar las fugas de memoria de referencias cíclicas. Sin embargo, desde PHP 5.3.0 se implementa el algoritmo síncrono del artículo » Recolección de ciclos concurrentes en sistemas de contabilidad de referencias que resuelve este asunto.

Una explicación detallada del funcionamiento del algoritmo queda más allá del objetivo de esta sección, aunque aquí explicaremos el mecanismo básico. Antes de nada, debemos establecer unas reglas básicas. Si se incrementa un refcount, este aún sigue en uso: no es basura. Si se decrementa el refcount y llega a cero, el zval puede liberarse. Esto significa que la recolección de ciclos sólo puede llevarse a cabo cuando un argumento refcount se decrementa a un valor que no sea cero. En segundo lugar, en un ciclo de recolección de basura, es posible averiguar qué partes son basura comprobando si se puede decrementar en uno sus refcount, para después comprobar cuáles zval poseen un refcount de cero.

Algoritmo de recolección de basura

Para evitar llamar a la comprobación de ciclos de basura en cada posible decremento de un refcount, el algoritmo lo que hace es pasar todas las posibles raíces (zvals) al "buffer raíz" (marcándolos en "púrpura"). También se asegura de que cada raíz de basura sólo finaliza una vez en el buffer. Únicamente cuando el buffer raíz está completo, comienza el mecanismo de recolección para todos los zval diferentes que haya en su interior. Véase el paso A en la figura anterior.

En el paso B, el algoritmo inicia una primera búsqueda en profundidad de todas las posibles raíces en las que decrementa por uno los refcount de los zval que encuentra, asegurándose de que no decrementa dos veces el mismo zval (marcándolo en "gris"). En el paso C, el algoritmo vuelve a llevar a cabo una búsqueda en profundidad dentro de cada nodo raíz, para volver a comprobar el refcount de cada zval. Si ve que el refcount es cero, se marca al zval en "blanco" (azul en la figura). Si es mayor que cero, deshace el decremento con una búsqueda en profundidad partiendo de ese punto, y se le vuelve a marcar en "negro". En el último paso (D), el algoritmo recorre el buffer raíz eliminando las raíces zval que haya, y a la vez, comprueba qué zvals se han marcado en "blanco" en el paso anterior. Todos los zval marcados en "blanco" se eliminarán.

Ahora que ya tiene un conocimiento básico de cómo funciona el algoritmo, volveremos atrás para ver cómo se integra esto en PHP. Por omisión, el recolector de basuras de PHP está habilitado. Hay, sin embargo, una directiva en php.ini que permite cambiar esto: zend.enable_gc.

Cuando el recolector de basura está habilitado, el algoritmo que busca ciclos, tal y como se ha descrito arriba, se ejecuta cada vez que se llena el buffer raíz. Éste tiene un tamaño fijo de 10.000 raíces posibles (se puede modificar esto cambiando la contante GC_ROOT_BUFFER_MAX_ENTRIES en Zend/zend_gc.c del código fuente de PHP, y recompilando PHP). Cuando el recolector de basuras se deshabilita, no se ejecutará el algoritmo que busca ciclos. Sea como fuere, las posibles raíces seguirían registrándose en el buffer raíz, sin importar si el mecanismo recolector de basuras está habilitado en la configuración o no.

Si estando deshabilitado el mecanismo recolector de basuras se llenara el buffer raíz de posibles raíces, no se registraría al resto de raíces posibles, por lo que no llegarían a ser analizadas por el algoritmo. Si fueran parte de un ciclo de referencia circular, nunca se liberarían y podrían provocar una fuga de memoria.

La razón por la que se registran las posibles raíces estando deshabilitado el mecanismo es porque es más rápido registrarlas que comprobar en cada una de ellas si el mecanismo está habilitado. Sin embargo, el recolector de basuras y el propio mecanismo de análisis, sí puede conllevar una cantidad de tiempo considerable.

Ademas de poder cambiar la configuración zend.enable_gc, también es posible habilitar o deshabilitar el mecanismo recolector de basura llamando a gc_enable() o gc_disable() respectivamente. La llamada a estas funciones tiene el mismo efecto que habilitar o deshabilitar el mecanismo en la propia configuración. También es posible forzar la recolección de ciclos incluso sin que esté lleno el buffer raíz. Para hacer esto, se puede usar la función gc_collect_cycles(). Esta función devuelve el número de ciclos que fueron recolectados por el algoritmo.

El motivo por el que es posible habilitar o deshabilitar el mecanismo, o iniciar los ciclos de recolección a mano, es porque podría haber determinadas partes de una aplicación que necesiten mucha precisión de tiempo. En estos casos, quizás no se desee que funcione el mecanismo recolector de basuras. Por supuesto, al deshabilitar el recolector de basuras en algunas partes del código, se corre el riesgo de provocar fugas de memoria, ya que algunas raíces podrían no caber en el buffer raíz. Por tanto, lo mas prudente sería llamar a gc_collect_cycles() justo después de llamar a gc_disable() para que libere la memoria ocupada por posibles raíces ya registradas en el buffer raíz. Esto deja por tanto un buffer vacío, de modo que queda más espacio para almacenar posibles raíces en el tiempo en que el mecanismo recolector de ciclos está deshabilitado.


Recolección de basura
PHP Manual