Cuando implementamos una clase, definimos sus atributos, sus
métodos, y por supuesto su(s) constructor(es). Y en ningún momento implementamos algo que nos permita DESTRUIR el objeto cuando ya no lo necesitemos. Esto se debe a que no tenemos una operación DISPOSE que nos permita liberar la memoria ocupada por un objeto justamente para evitarnos el tener que preocuparnos por la memoria usada por nuestros programas. De este modo, al menos en objetos simples no tiene sentido una operación que los destruya, es más, no tenemos cómo implementarla. ¿Cómo liberamos entonces la memoria usada por nuestros objetos? Pues en otros lenguajes tenemos que tener especial cuidado de no dejar colgados a nuestros objetos porque luego quedaban inaccesibles y ocupando lugar de memoria. Esto claramente es una mala práctica de programación. Por ejemplo, si en Modula hacíamos: p= CrearPersona(“Gaspar”,”Henaine”);
Primero hemos creado un objeto Persona referenciado por
p y luego hemos creado otro objeto Persona referenciado también por p. Esto hace que p deje de apuntar al primer objeto creado y quede apuntando al segundo. De este modo, el objeto con nombre Gaspar Henaine queda perdido en memoria, inaccesible y ocupando espacio. Lo correcto habría sido destruir el objeto Persona Gaspar Henaine antes de crear el nuevo, o bien, referenciar dicho objeto con otra variable. En Java para liberar memoria justamente tenemos que dejarla colgada. Sí, tenemos que dejar memoria colgada porque Java será quién se encargue de liberarla luego, nosotros no tenemos control sobre eso. De este modo, si yo hago en Java algo como esto: p= CrearPersona(“Gaspar”,”Henaine”);
p= null;
estoy dejando el objeto creado en la primera línea colgado en
memoria e inaccesible. Justamente eso es lo que Java necesita saber para liberar esa memoria luego. O sea que, para liberar memoria tengo que, a propósito, dejarla colgada. Así, todo lo que estaba mal en otros lenguajes ahora en Java no nos da problemas. Por ejemplo: p= new Persona(“Gaspar”,”Henaine”); q= p; p= null; Allí hemos dejado la referencia p en null pero no hemos afectado a q por tanto el objeto creado no está inaccesible y por ende no está colgado en memoria.
Entonces ¿cómo funciona realmente esto de la
gestión de memoria en Java? Recolección de basura
Como estamos viendo, para liberar memoria tenemos que
dejarla colgada, no tenemos otro modo. Esto es porque en Java existe un proceso llamado Garbage Collector (recolector de basura) que cada tanto tiempo se ejecuta y busca en memoria los objetos que están inaccesibles desde el programa principal liberando la memoria ocupada por ellos, es decir, este proceso se encarga justamente de buscar y liberar todo lo que hemos dejado colgado y que por tanto se considera basura. Entonces, el recolector de basura elimina de memoria todo aquello que no está referenciado por nadie o bien, que no es accesible desde el programa principal ¿Cuándo pasa el recolector de basura?
Este proceso es ejecutado por la máquina virtual de Java y
nosotros como programadores no tenemos ningún control sobre él, por tanto se ejecuta esporádicamente o cuando el sistema necesita memoria para otra cosa. Nunca se sabe entonces cuando será ejecutado este proceso. Otro punto importante es que la ejecución del recolector de basura no implica necesariamente que se eliminen todos los objetos que son considerados basura. Por tanto, si hemos dejado cinco objetos colgados, cuando el recolector pase no tiene por qué eliminar los cinco objetos. Nosotros tampoco tenemos un control sobre eso. Existe una instrucción que podemos utilizar para indicar a la máquina virtual de Java que queremos que el recolector de basura pase para limpiar la memoria la cual es:
System.gc();
Sin embargo enfatizaré específicamente la parte de que con
esto indicamos a la máquina virtual que QUEREMOS que el recolector pase, pero no implica que la máquina virtual lo ejecute y por tanto esa decisión dependerá de ella. De este modo, por mucho énfasis que pongamos en querer liberar memoria nunca sabremos efectivamente cuando será ejecutado el recolector de basura. Este proceso es muy inteligente, en el siguiente sentido:
Si tenemos por ejemplo una lista ligada y perdemos la
referencia al primer nodo estamos dejando entonces todo el contenido de la lista colgado en memoria. En un caso así, a pesar de que cada nodo referencia al siguiente y por ende existen objetos que son referenciados por alguien, el recolector de basura puede determinar que en realidad no podemos llegar a ninguno de ellos desde el programa principal.
Lo mismo sucede con una lista circular, un árbol binario, o
cualquier estructura de memoria dinámica. Esto implica entonces que el recolector de basura puede determinar cuando toda una enorme estructura llena de punteros que referencian a objetos de todos lados son basura o no. Si a es una referencia a un árbol binario de búsqueda que contiene miles de nodos y yo hago a=null, el recolector de basura podrá determinar que no es posible llegar a ningún nodo del árbol desde el programa principal y por tanto lo eliminará todo, ya no tenemos que programarlo nosotros. La contraparte de esto es que la recolección de basura es entonces un proceso muy pesado y que consume recursos, por este motivo es la máquina virtual la que decide cuando es necesario ejecutarlo, lo cual dependerá de la necesidad del sistema operativo por usar la memoria ocupada, la carga del procesador en el momento actual (si el procesador está muy ocupado no conviene ejecutar el recolector), la necesidad de nuestro programa por obtener nueva memoria, etc. Toda esta complicación queda por parte de los programadores de Java y por tanto nosotros solo la utilizamos. Ejemplo: Crearemos entonces cuatro objetos de tipo Persona y luego los eliminaremos, es decir, los desreferenciaremos con el fin de que queden como basura, inaccesibles por nosotros y por tanto nos desentenderemos de ellos porque sabemos que Java los eliminará en algún momento: public class DatosPersonas { public static void main(String[] args){ Persona p1= new Persona("Gaspar","Henaine"); Persona p2= new Persona("Gaspar","Henaine"); Persona p3= new Persona("Gaspar","Henaine"); Persona p4= new Persona("Gaspar","Henaine"); System.out.println(Persona.obtenerCantidadPersonas()); p1= null; p2= null; p3= null; p4= null; } } Hasta ahí todo bien, sin embargo si ustedes vuelven a mostrar en pantalla el valor de la variable cantidadPersonas verán que vuelve a salir el número 4. Entonces en realidad esta variable lleva un conteo de los objetos instanciados desde el inicio del programa sin tomar en cuenta los eliminados. De este modo si a lo largo del tiempo de ejecución de mi programa creo en total 1500 objetos, sea que hayan convivido en memoria todos a la vez o no, la variable marcará el valor 1500; más claramente, suma 1 cada vez que creamos un objetos, jamás disminuye. ¿Cómo hacemos para restar 1 a la variable cuando se destruya un objeto?
Deberíamos saber cuando el recolector de basura elimina
efectivamente a un objeto en memoria que es considerado basura.
¿Cómo logramos esto?
Pues Java nos provee de una operación ya definida que se
ejecuta cuando un objeto va a ser eliminado, es decir, cuando el recolector de basura va a reclamar la memoria ocupada por un objeto que es basura este tiene la posibilidad de ejecutar una última operación antes de ser borrado. Esta operación se conoce con el nombre finalize. La razón de la existencia de esta operación es darle al programador la posibilidad de liberar algún posible recurso que el objeto pueda estar usando antes de ser eliminado con el fin de tener una buena gestión sobre ese recurso; un ejemplo podría ser la conexión con una base de datos que debería ser cerrada antes de eliminar al objeto que la representa.
En este caso puntual nosotros usaremos la operación finalize
para restar 1 a la variable cantidadPersonas a fin de que represente realmente la cantidad de objetos de la clase Persona que existen en memoria en un momento dado. La declaración de la operación finalize es: public void finalize()
Entonces en nuestra clase Persona declaremos esta
operación dándole además un método como el que muestro ahora: @Override public void finalize(){ Persona.cantidadPersonas--; }
Vallamos ahora a la clase principal de DatosPersonas y
agreguemos estas dos líneas a lo que ya teníamos:
System.gc();
System.out.println(“Objetos en memoria ”+Persona.obtenerCantidadPersonas());
¿Qué hicimos? Pues agregamos un llamado al recolector de basura para intentar que el sistema lo ejecute.
Luego mostramos en pantalla cuantos objetos quedan
efectivamente en memoria luego del llamado. Ejecuten el programa varias veces y verán que el resultado puede variar. Eso dependerá de si realmente el recolector de basura fue ejecutado y además, en caso afirmativo, dependerá de si fueron eliminados todos los objetos basura. Tengan en cuenta que la variable cantidadPersonas indicará entonces la cantidad de objetos de tipo Persona que existan en memoria en un momento dado, sea que nosotros tengamos referencias a ellos o no. No es lo mismo que llevar un registro de la cantidad de objetos Persona que nosotros tenemos referenciados. public class DatosPersonas { public static void main(String[] args){ Persona p1= new Persona("Gaspar","Henaine"); Persona p2= new Persona("Gaspar","Henaine"); Persona p3= new Persona("Gaspar","Henaine"); Persona p4= new Persona("Gaspar","Henaine"); System.out.println(Persona.obtenerCantidadPersonas()); p1= null; p2= null; p3= null; p4= null; System.gc(); System.out.println(“Objetos en memoria ”+Persona.obtenerCantidadPersonas()); } }