Recolector de Basura de PHP5
A partir de una pregunta en el Curso de PHP que estoy impartiendo, me surgieron dudas acerca del funcionamiento del recolector de basura de PHP5.
Gratamente sorprendido, descubro que la implementación que PHP5 hace del recolector de basura es bastante mejor de lo esperado, aunque no exenta de algunas deficiencias.
Vamos a mostrar a continuación un ejemplo práctico de como funciona el recolector:
Probando el recolector de basura
PHP5 implementa un método __destruct() para los destructores de las clases, de manera que podemos especificar el modo en que se destruyen los objetos cuando se dejan de utilizar. Según la documentación este método se llama cuando desaparecen todas las referencias a una instancia del objeto.
Hagamos un script de prueba para verificar que esto es así:
[code lang=”php”]
Class Car {
public $name;
function __construct($name) {
$this->name = $name;
}
function __destruct() {
echo “CAR:*crash* this car ({$this->name}) has been destroyed”;
}
function drive() {
echo ‘CAR:Vrooooom… ‘ . $this->name . “”;
}
}
class Garage
{
public $car;
function __construct()
{
$this->car = new Car(‘Jaguar’);
}
function park(Car $car)
{
$this->car=$car;
echo __CLASS__.”: Parking {$car->name} “;
}
function __destruct() {
echo “GARAGE: *crash* this garage has been destroyed”;
}
}
class Parking extends Garage
{
function pay()
{
echo “PARKING: Paying fee”;
}
function __destruct() {
parent::__destruct();
echo “PARKING: *crash* this parking has been destroyed”;
}
}
// Instancia 1
$porsche = new Car(‘Porsche’);
$porsche->drive();
$myGarage=new Garage(); // Por defecto el garaje crea un coche.
$a = new Car(“Seiscientos”);
echo “INFO: Aparcamos el seiscientos. El jaguar debería morir.”;
$myGarage->park($a); // Aparcamos el seiscientos. El jaguar debería morir.
echo “INFO: Hacemos el unset de $a”;
unset($a); // Todavía queda una referencia, no lo debería destruir
echo “INFO: Nombre del coche en el garaje: {$myGarage->car->name}”;
echo “INFO: Aparcamos un nuevo coche, y el seiscientos se queda sin referencias”;
$myGarage->park(new Car(“Audi”)); // El seiscientos debería morir
echo “INFO: Creamos un parking”;
$b = new Parking();
echo “INFO: Y destruimos el parking”;
unset($b);
echo ‘Salimos del script, y los objetos que queden se destruyen solos (porsche y Garage con audi)’;
[/code]
Con este código verificamos que se comporta como es lo esperado. Si por casualidad no hemos destruido explícitamente una instancia, se destruye al finalizar el script.
Este es el resultado obtenido en la salida:
[code lang=”php”]
CAR:Vrooooom… Porsche
INFO: Aparcamos el seiscientos. El jaguar debería morir.
CAR:*crash* this car (Jaguar) has been destroyed
Garage: Parking Seiscientos
INFO: Hacemos el unset de $a
INFO: Nombre del coche en el garaje: Seiscientos
INFO: Aparcamos un nuevo coche, y el seiscientos se queda sin referencias
CAR:*crash* this car (Seiscientos) has been destroyed
Garage: Parking Audi
INFO: Creamos un parking
INFO: Y destruimos el parking
GARAGE: *crash* this garage has been destroyed
PARKING: *crash* this parking has been destroyed
CAR:*crash* this car (Jaguar) has been destroyed
Salimos del script, y los objetos que queden se destruyen solos (porsche y Garage con audi)
GARAGE: *crash* this garage has been destroyed
CAR:*crash* this car (Audi) has been destroyed
CAR:*crash* this car (Porsche) has been destroyed
[/code]
¿Qué ha pasado aquí? Vamos por partes:
Hemos definido las clases Car, Garage y Parking. Todas implementan destructores sencillos, que simplemente vuelcan un mensaje en la salida. Nuestro objetivo es verificar cuando se llama el destructor, así que no nos vamos a complicar la vida de momento, y usamos estas clases sencillas.
- Car es una clase que tiene un nombre de coche, y un método drive(). No hace nada más. Su destructor avisa de que se ha descargado la clase.
- Garage es una clase que tiene una propiedad pública con una instancia Car. La clase Garage cuando se instancia automáticamente carga un Jaguar. Podemos aparcar otro coche en el garaje con el método park(). Esta clase tiene un destructor, que no hace nada más que mostrar un mensaje.
- Parking es una clase que hereda de Garage. Su destructor tienen que llamar explícitamente al destructor de la clase padre, o éste no se ejecutará.
En la primera parte del código es donde definimos estas clases:
[code lang=”php”]
Class Car {
public $name;
function __construct($name) {
$this->name = $name;
}
function __destruct() {
echo “CAR:*crash* this car ({$this->name}) has been destroyed”;
}
function drive() {
echo ‘CAR:Vrooooom… ‘ . $this->name . “”;
}
}
class Garage
{
public $car;
function __construct()
{
$this->car = new Car(‘Jaguar’);
}
function park(Car $car)
{
$this->car=$car;
echo __CLASS__.”: Parking {$car->name} “;
}
function __destruct() {
echo “GARAGE: *crash* this garage has been destroyed”;
}
}
class Parking extends Garage
{
function pay()
{
echo “PARKING: Paying fee”;
}
function __destruct() {
parent::__destruct();
echo “PARKING: *crash* this parking has been destroyed”;
}
}
[/code]
A continuación vamos a hacer uso de estas clases. Creamos unas cuantas instancias de clases para poder jugar con ellas.
[code lang=”php”]
$porsche = new Car(‘Porsche’);
$porsche->drive();
$myGarage=new Garage(); // Por defecto el garaje crea un coche.
$a = new Car(“Seiscientos”);
[/code]
Tenemos nuestra instancia de Car que es un Porsche, una instacia de Garage, y otra instancia de Car que es un Seiscientos. Además, dentro de $myGarage tenemos una instancia de Car que es un Jaguar.
Lo que hacemos a continuación es aparcar el Seiscientos en el garaje. en el momento que asignemos la propiedad $myGarage->car a la instancia del Seiscientos, la instancia del Jaguar se quedará sin referencias, y deberá llamar al destructor:
[code lang=”php”]
echo “INFO: Aparcamos el seiscientos. El jaguar debería morir.”;
$myGarage->park($a); // Aparcamos el seiscientos. El jaguar debería morir.
[/code]
Efectivamente es lo que ocurre, según los mensajes que obtenemos:
[code]
INFO: Aparcamos el seiscientos. El jaguar debería morir.
CAR:*crash* this car (Jaguar) has been destroyed
Garage: Parking Seiscientos
[/code]
Esto es muy buena noticia. Acabamos de ver que el objeto (el Jaguar) se elimina justo cuando se queda sin referencias. ¿Qué ocurrirá si destruyo ahora $a?. No debería pasar nada, ya que unset($a) rompe solo la asociación entre la variable y el contenido, no el contenido en sí. Además, la instancia de Seiscientos sigue manteniendo una referencia desde dentro de la instancia $myGarage:
[code lang=”php”]
echo “INFO: Hacemos el unset de $a”;
unset($a); // Todavía queda una referencia, no lo debería destruir
echo “INFO: Nombre del coche en el garaje: {$myGarage->car->name}”;
[/code]
Verificamos que todo está en orden:
[code lang=”php”]
INFO: Hacemos el unset de $a
INFO: Nombre del coche en el garaje: Seiscientos
[/code]
A continuación vamos a crear un nuevo coche, Audi, y aparcarlo en el garaje. Al igual que pasó antes con el Jaguar, el Seiscientos debe llamar a su destructor al quedarse sin referencias.
[code lang=”php”]
echo “INFO: Aparcamos un nuevo coche, y el seiscientos se queda sin referencias”;
$myGarage->park(new Car(“Audi”)); // El seiscientos debería morir
[/code]
Y efectivamente:
[code]
INFO: Aparcamos un nuevo coche, y el seiscientos se queda sin referencias
CAR:*crash* this car (Seiscientos) has been destroyed
Garage: Parking Audi
[/code]
Ahora vamos a probar la herencia de los destructores. Al igual que los constructores, hay que llamar explícitamente a los destructores de la clase padre, o no se ejecutará su destructor. Nuestra clase Parking hace una llamada al destructor de Garage, y obtenemos un comportamiento correcto:
[code lang=”php”]
echo “INFO: Creamos un parking”;
$b = new Parking();
echo “INFO: Y destruimos el parking”;
unset($b);
[/code]
Como resultado vemos que se ejecutan los dos destructores,y se destruye el coche que teníamos dentro (el Jaguar):
[code lang=”php”]
INFO: Creamos un parking
INFO: Y destruimos el parking
GARAGE: *crash* this garage has been destroyed
PARKING: *crash* this parking has been destroyed
CAR:*crash* this car (Jaguar) has been destroyed
[/code]
Si modificamos el código del destructor de Parking, y comentamos la llamada al destructor padre, no se ejecuta:
[code lang=”php”]
class Parking extends Garage
{
function pay()
{
echo “PARKING: Paying fee”;
}
function __destruct() {
//parent::__destruct();
echo “PARKING: *crash* this parking has been destroyed”;
}
}
[/code]
Y verificamos:
[code lang=”php”]
PARKING: *crash* this parking has been destroyed
CAR:*crash* this car (Jaguar) has been destroyed
[/code]
El recolector de basura, aunque no ha ejecutado del destructor de la clase Garage, si ha destruido la instancia, y por lo tanto la referencia al Jaguar, que también ha sido destruido. Hay que tener cuidado con esto, ya que si liberamos algún tipo de recurso especial en nuestra clase padre, no se realizará.
Las instancias de $myGarage y $porsche, que no habían sido destruidas explícitamente, se destruyen automáticamente cuando se termina la ejecución del script.
[code lang=”php”]
Salimos del script, y los objetos que queden se destruyen solos (porsche y Garage con audi)
GARAGE: *crash* this garage has been destroyed
CAR:*crash* this car (Audi) has been destroyed
CAR:*crash* this car (Porsche) has been destroyed
[/code]
¿Dónde están los problemas?
Los objetos del tipo resource no son manejados correctamente por ell recolector de basura. Si bien la mayoría de los recursos son eliminados de la misma manera que las clases, cuando no quedan referencias al recurso, algunos de ellos no corren la misma suerte. Por ejemplo, las conexiones a bases de datos a pesar de haber perdido su referencia no son destruidas, o las imágenes de la extensión GD. Para eliminar este tipo de recursos habrá que construirse un recolector de basura propio (con register_shutdown_function) o, mejor aún, seguir una norma básica:
DESTRUIR TUS OBJETOS Y RECURSOS CUANDO NO LOS VAYAS A UTILIZAR MÁS.
La propia filosofía de programación de PHP implica que en algún momento dado terminaremos nuestra respuesta de manera consciente. En este momento es cuando deberemos cerrar todos los recursos abiertos, destruyendo de manera explicita objetos y recursos.
Otro problema que surge es la recolección de basura en el objeto $_SESSION. El recolector de basura no funciona de la misma manera para el objeto $_SESSION que para el resto de la aplicación. Su comportamiento dependerá de la configuración que tengamos en el fichero de php.ini. Puede que esté desactivado, o que no tenga una configuración adecuada para nuestra vida de sesión. Si hemos elegido almacenar las sesiones en subdirectorios, nos veremos obligados a implementar nuestro propio recolector de basura por medio de un proceso programado (con cron, u otro).