Limpieza de Ciclos
Tradicionalmente, los mecanismos de conteo de referencias, como los utilizados anteriormente
en PHP, no saben manejar las fugas de memoria debidas a referencias circulares;
sin embargo, desde PHP 5.3.0, un algoritmo síncrono derivado del análisis
» Concurrent Cycle Collection in Reference Counted Systems
se utiliza para abordar este problema en particular.
Una explicación completa del funcionamiento del algoritmo iría un poco más allá del alcance de esta sección,
pero aquí presentaremos los principios básicos. En primer lugar, estableceremos algunas reglas básicas.
Si un refcount se incrementa, el contenedor siempre se utiliza, por lo tanto, no se limpia. Si el refcount
se decrementa y llega a cero, el contenedor zval puede ser eliminado y la memoria liberada. En primer lugar, esto significa
que los ciclos perturbadores solo pueden crearse cuando el refcount se decrementa a un valor
diferente de cero. Luego, en un ciclo problemático, es posible detectar la basura verificando si es posible o no
decrementar su refcount en uno, verificando luego qué zvals tienen un refcount a cero.
Para evitar tener que llamar a la rutina de limpieza en cada decrementación de refcount posible,
el algoritmo coloca todas las raíces zval en un "búfer de raíces" (marcándolas en "violeta").
También se asegura de que cada raíz aparezca solo una vez en el búfer.
El mecanismo de limpieza solo interviene cuando el búfer está lleno. Vea el paso A
en la figura anterior.
En el paso B, el algoritmo lanza una búsqueda en todas las raíces posibles, para
decrementar en una unidad los refcounts de todas las zvals que encuentra, teniendo mucho
cuidado de no decrementar dos veces el refcount de la misma zval (marcándolas
como "grises"). En el paso C, el algoritmo relanza una búsqueda en todas las raíces
posibles y escanea el valor de refcount de cada zval. Si encuentra un refcount a cero,
la zval se marca como "blanca" (azul en la figura). Si encuentra un valor superior a cero,
cancela la decrementación del refcount realizando una búsqueda desde este nodo, y las
marca como "negras" nuevamente. En el último paso, D, el algoritmo recorre todo el
búfer de raíces y las elimina, escaneando cada zval; cualquier zval marcada como "blanca" en el
paso anterior será entonces eliminada de la memoria.
Ahora que se sabe globalmente cómo funciona el algoritmo, se verá cómo se ha integrado en PHP. Por omisión, el recolector de basura de PHP está
activado. Sin embargo, hay una opción de php.ini para cambiar esto:
zend.enable_gc.
Cuando el recolector de basura está activado, el algoritmo de búsqueda de ciclos
descrito anteriormente se ejecuta cada vez que el búfer está lleno. El búfer de
raíces tiene un tamaño fijado a 10.000 raíces (este parámetro es modificable gracias a
GC_THRESHOLD_DEFAULT
en Zend/zend_gc.c
en el código fuente de PHP, por lo tanto, se necesita una recompilación). Si el recolector de
basura está desactivado, la búsqueda de ciclos también lo está. Sin embargo, las raíces
posibles siempre se registrarán en el búfer, esto no depende de la activación
del recolector de basura.
Si el búfer está lleno mientras el mecanismo de limpieza está desactivado, las raíces
ya no se registrarán. Estas raíces nunca serán analizadas por el algoritmo, y si
formaban parte de referencias circulares, nunca se limpiarán, y causarán fugas de memoria.
La razón por la que las raíces posibles se registran en el búfer
incluso si el mecanismo está desactivado es que habría sido demasiado costoso verificar la posible
activación del mecanismo en cada intento de agregar una raíz al búfer. El mecanismo
de recolección de basura y análisis puede, por su parte, ser muy costoso en tiempo.
Además de poder cambiar el valor del parámetro de configuración
zend.enable_gc, también se puede activar o desactivar el mecanismo de
recolección de basura llamando a las funciones gc_enable() o
gc_disable() respectivamente. Utilizar estas funciones tendrá el mismo efecto que modificar el parámetro de configuración. También se tiene la posibilidad de forzar la ejecución del
recolector de basura en un momento dado en el script, incluso si el búfer aún no
está completamente lleno. Para ello, utilice la función gc_collect_cycles(),
que devolverá el número de ciclos recolectados.
Se puede tomar el control desactivando el recolector de basura o forzándolo a pasar en un momento dado porque algunas partes de la aplicación
podrían depender fuertemente del tiempo de procesamiento, en cuyo caso se podría desear que el recolector de basura no se inicie. Por supuesto, al desactivar el recolector de basura para algunas partes de la aplicación, se corre el riesgo de crear
fugas de memoria, ya que algunas raíces probables podrían no registrarse en el búfer de memoria de tamaño limitado.
En consecuencia, generalmente se recomienda desencadenar manualmente el proceso gracias a
gc_collect_cycles() justo antes de la llamada a
gc_disable(), para liberar memoria. Esto dejará un búfer
vacío, y habrá más espacio para raíces probables cuando
el mecanismo esté desactivado.