¿Por qué debería importarnos la memoria en PHP?
\nLa mayoría de los desarrolladores de PHP piensan que la memoria es un recurso infinito. ¿Necesitas un array de un millón de elementos? Adelante. ¿Creaste un objeto y te olvidaste de él? No pasa nada, PHP lo limpiará por ti. Pero cuando tu proyecto empieza a fallar con el error Allowed memory size exhausted o se ralentiza sin motivo aparente, es hora de entender qué está pasando realmente bajo el capó.
Hoy vamos a abrir la caja negra llamada «Memoria y recolector de basura en PHP». Aprenderás cómo PHP cuenta las referencias, por qué las referencias cíclicas son problemáticas y cómo hacer que el recolector de basura trabaje a tu favor. ¡Allá vamos!
\n\n¿Cómo gestiona PHP la memoria? De la petición a la respuesta
\nA diferencia de C o C++, donde tú mismo asignas y liberas la memoria, PHP funciona a través de Zend Engine. Cada proceso de PHP tiene su propio pool de memoria. Cuando escribes:
\n$data = [1, 2, 3];\nPHP asigna memoria para el array, guarda los valores e incrementa el llamado refcount (contador de referencias). Mientras alguien haga referencia a la variable $data, la memoria no se libera.
\nPero en cuanto haces:
\nunset($data);\nel contador de referencias baja a cero y Zend Engine devuelve la memoria al pool de inmediato. Sencillo y rápido. Sin embargo, hay matices…
\n\nAritmética de referencias: refcount e is_ref
\nCada variable en PHP es una estructura zval que almacena el valor, el tipo, la bandera is_ref (si es una referencia) y refcount. Veamos un ejemplo:
$a = "Hola";\n$b = $a; // refcount aumenta a 2 (copia diferida)\n$b = "Mundo"; // ahora $a y $b son zval diferentes, refcount de "Hola" baja a 1\n\nEsto se llama copy-on-write (copia en escritura). Mientras no modifiques el valor, PHP ahorra memoria usando la misma zona. Pero en cuanto cambias la variable, se crea una copia. ¿Ingenioso, verdad?
\n\nEl recolector de basura (GC): cuando refcount no es suficiente
\nEl mecanismo normal de conteo de referencias funciona perfectamente en el 99% de los casos. Pero hay un problema: las referencias cíclicas. Imagina que tienes dos objetos que se referencian mutuamente:
\nclass Nodo {\n public $siguiente;\n}\n\n$a = new Nodo();\n$b = new Nodo();\n$a->siguiente = $b;\n$b->siguiente = $a;\n\nunset($a);\nunset($b);\n\nDespués de unset, ambos objetos siguen teniendo refcount = 1 (cada uno apunta al otro). Formalmente son inaccesibles desde el código, pero la memoria no se libera. Eso es una referencia cíclica.
Si hay muchos objetos así, la memoria se fuga. Para combatir esto, a partir de PHP 5.3 se incorporó un recolector de basura (GC) completo.
\n\n¿Cómo funciona el GC en PHP?
\nEl recolector de basura no se ejecuta en cada línea de código. Se activa cuando el búfer de raíces (el array de posibles referencias cíclicas) se llena. Por defecto, su tamaño es de 10.000 elementos. Cuando el búfer está lleno, el GC ejecuta un algoritmo que:
\n- \n
- Escanea las raíces: encuentra todos los zval con refcount > 0 que podrían ser parte de un ciclo. \n
- Reduce el refcount de todos los elementos hijos (como si no hubiera referencias). \n
- Verifica: si después de la reducción el refcount llega a cero, significa que es «basura» y se puede eliminar. \n
- Restaura el refcount de los que no resultaron ser basura y libera la memoria. \n
Este proceso se llama garbage collection. Puedes ejecutarlo manualmente:
\ngc_collect_cycles();\nPero no lo hagas a la menor provocación: es una operación costosa.
\n\n¿Cómo detectar fugas de memoria en PHP?
\nSi tu script se ejecuta durante mucho tiempo (por ejemplo, un worker de cola o un demonio), las fugas se vuelven críticas. Aquí tienes algunas herrami