Garbage Collector en Java

Buenas tardes, en esta entrada hablaremos cobre el Garbage Collector, o GC en Java. Veremos para que sirve, cuando se ejecuta y su finalidad principal.


Resumen de la Administración de Memoria y Garbage Collection

Finalmente vamos a meternos en el maravilloso mundo de la administración de la memoria y el recolector de basura.

Consideremos que tenemos un programa que lee una gran cantidad de información, digamos que de algún lado en la red, y entonces escribe todo esos datos en una base de datos en el disco duro. Un diseño típico sería capaz de leer la información en algún tipo de colección en memoría, realizar algunas operaciones con la información, y entonces escribir los datos en la base de datos. Después de que la información se haya escrito en la base de datos, la colección que ha guardado la información temporalmente deberá estar lleno con información antigua o borrada y debe ser recreada para el siguiente lote. Esta operación podría ser realizada millones de veces, y en lenguajes como C o C++ que no ofrecen una manera automática de recoger la basura, una pequeña imperfección en la lógica que manualmente borra o vacía la estructura de datos en la colección puede permitir pequeños montones de memoria a que sean impropiamente reclamados o pordidos, para siempre. Estas pequeñas pérdidas de memoria son llamadas “Memory Leak”, y tras varios cientos de iteraciones pueden hacer que exista una cantidad suficiente de memoria inaccesible que hará que el programa se cierre. Crear código que realice una administración de memoria manual de manera limpia no es trivial y muy complejo, y si bien las estimaciones varían, es discutible que la administración manual de la memoria puede doblar el esfuerzo del desarrollo para un programa complejo.

El GC (Garbage Collector) de Java provee una solución automática de administración de memoria. En la mayoría de los casos nos libera de tener que añadir cualquier lógica de administración de memoria a nuestra aplicación. La desventaja del GC automático es que no podemos completamente controlar cuando see jecuta y cuando no lo hace.

Resumen del GC de Java

Vamos a ver que significa cuando hablamos sobre recolector de basura en los campos deJava. El recolector de basura es la frase que se usa para describir la administración de memoria automática en Java. Dondequiera que se ejecute un programa (Ya sea en Java, C, C++, Lisp, Ruby, etc), este usa memoria de diferentes maneras. Es típico para la memoria crear un stack, un heap, en el caso de Java crear constant pools y áreas de método. El Heap es la parte de la memoria donde los objetos de Java viven, y es la única que de cualquier manera está envuelta en el proceso de recolección de basura.

Así, todo gira en torno a la recolección de basura asegurandose que el Heap tiene suficiente espacio libre como le sea posible. Cuando el GC se ejecuta, su propósito es buscar y borrar los objetos que no pueden ser alcanzados. Si pensamos que un programa en Java es como un ciclo constante de creación de objetos que se necesitan (los cuales ocupan espacio en el heap), y entonces se van descargando los objetos que ya no son necesitados, creando nuevos objetos, descartandolos, y así, la pieza que nos falta para completar el puzzle es el GC. Cuando se ejecuta, busca todos esos objetos descartados y los borra de la memoria para que el ciclo de uso de memoria y liberandose pueda continuar.

¿Cuando se ejecuta el GC?

El G está bajo el control de la JVM. La JVM decide cuando se ejecuta el GC. Desde dentro de nuestro programa Java podemos decirle a la JVM que ejecute el GC, pero no hay garantías, bajo ninguna circunstancia de que la JVM cumpla con esto. La JVM ejecutará típicamente el GC cuando sienta que la memoría está siendo baja. La experiencia nos indica que cuando nuestro programa Java solicita una recolección de basura, la JVM nos garantizará nuestra solicitud a corto plazo, pero no hay garantías de ello.

¿Cómo funciona el GC?

No podemos estar seguros. Podemos haber escuchado que el GC usa un algoritmo de marca y barrido, y para cualquier implementación de Java dada esto puede ser verdad, pero la especificación de Java no garantiza cualquier implementación particular. Podemos haber oido que el GC usa un contador de referencia; una vez mas puede ser o no puede ser. El concepto importante es entender cuando un objeto se convierte en elegible para el GC. Para responder a esto deberemos dar un salto y hablar un poco sobre los Threads. Los programas en Java, tienen desde uno hasta varios threads (hilos). Cada thread tiene su propio stack de ejecución. normalmente, el programador hace que se ejecute un hilo en Java, el que usa el método main(). Hay mas razones por las que se puede lanzar threads adicionales desde neustro thread principal. En adición a tener su propio stack de ejecución, cada thread tiene su propio ciclo de vida. Por ahora, todo lo que necesitamos saber es que los threads pueden estar vivos o muertos. Con esta información de fondo, podemos decir con claridad que un objeto es elegible para el GC cuando ningún thread vivo puede acceder a el.

Basados en esta definición, el GC hace algo mágico, operaciones desconocidas, y cuando descubre un objeto que no puede ser alcanzado por cualquier thread vivo, se considera que el objeto es elegible para el borrado. Cuando hablamos sobre alcanzar un objeto, estamos hablando realmente sobre tener al alcance su variable de referencia que refiere al objeto en cuestión. Si nuestro programa Java tiene una variable de referencia que se refiere a un objeto, y esa variable de referencia está disponible en un thread vivo, entonces el objeto es considerado alcanzable. Hablaremos mas sobre como los objetos pueden ser inalcanzables en la siguiente sección.

¿Puede una aplicación Java quedarse sin memoria? Si. El sistema de recolección de basura intenta eliminar objetos de la memoria cuando no son usados. Sin embargo, si mantenemos muchos objetos viviendo el sistema puede quedarse sin memoria. La recolección de basura no puede asegurar que haya suficiente memoria, solo que la memoria que esté disponible pueda seradministrada de la manera mas eficiente como sea posible.

Escribir código que explícitamente haga objetos elegibles para la Recolección

En la sección anterior, aprendimos las teorías tras el GC de Java. En esta sección, vamos a ver como hacer que los objetos sean elegibles para la recolección usando código actual. También discutiremos como intentar a forzar la recolección si es necesario, y como podemos realizar limpieza adicional en objetos antes de que sean removidos de la memoria.

Haciendo Null una referencia

Como discutimos anteriormente, un objeto se convierte en elegible para la recolección de basura cuando no hay mas referencias accesibles a el. Obviamente, si no hay referencias accesibles, no importa que le pase al objeto. Para nuestro propósito es solo algo flotando en el espacio, sin usar, inaccesible, y que no se necesite más.

La primera forma de remover una referencia a un objeto es establecer su variable de referencia para que el objeto se refiera a null. Veamos el siguiente ejemplo:

 
	public class GarbageTruck { 
		public static void main (String[] args){ 
			StringBuffer sb = newStringBuffer("Hola");
			System.out.println(sb); // El objeto StringBuffer no es elegible para la
			recolección sb = null; // Ahora el objeto StringBuffer es elegible para la recolección 
		} 
	}
	

El objeto StringBuffer con el valor “hola” es asignado a la vriable de referencia sb en la tercera línea. Para hacer el objeto elegible para el GC, establecemos la variable de referencia sb a null, lo cual remueve la referencia que existía al objeto StringBuffer. Una vez que la línea 6 se ha ejecutado, nuestro objeto StringBuffer es elegible para la recolección.

Reasignando a la Variable de Referencia

Podemos tambien desacoplar una variable de referencia de un objeto estableciendo la variable de referencia para que refiera a otro objeto. Veamos el siguiente ejemplo de código:

 
	public class GarbageTruck { 
		public static void main (String[] args){ 
			StringBuffer s1 = new
			StringBuffer("Hola"); StringBuffer s2 = new
			StringBuffer("Adios"); System.out.println(s1); // El StringBuffer "hola" no es elegible
			s1 = s2; // Redireccionamos s1 para que se refiera al objeto "Adios" 
			// Ahora el StringBuffer "Hola" es elegible para la recolección 
		} 
	} 
	

Los objetos que son creados en un método también necesitan ser considerados. Cuando un método es invocado, cualquier variable local creada existe solo mientras el método exista. Una vez que el método ha retornado, el objeto creado en el método es elegible para el GC. Hay una excepción obvia, sin embargo. Si un objeto es retornado desde un método, su referencia puede ser asignada a una variable de referencia en el método que lo ha llamado; por lo tanto, no será elegible para la recolección. Veamos el siguiente código:

	public class GarbageFactory { 
		public static void main (String[] args){ 
			Date d = getDate();
			doComplicatedStuff(); 
			System.out.println("d = " +d); 
		} 
		
		public static Date getDate(){ 
			Date d2 = new Date(); 
			StringBuffer now = new
			StringBuffer(d2.toString());
			System.out.println(now); 
			return d2; 
		} 
	}
	

En el ejemplo anterior, hemos creado un método llamado getDate() que retorna un objeto Date. Este método crea 2 objetos, uno Date y otro StringBuffer que contiene la información de la fecha. Ya que el método retorna un objeto Date, no será elegible para la recolección incluso después de haber terminado el método. El objeto StringBuffer, al contrario, si será elegible, aunque no hayamos establecido explicitamente la variable now a null.

Aislar una Referencia

Hay otra manera por la cual los objetos pueden convertirse en elegibles para la recolección, incluso si siguen manteniendo referencias válidas. A este escenario le llamaremos “Islas de Aislamiento”.

Un ejemplo simple es una clase que tiene una variable de instancia que es una variable de referencia a otra instancia de la misma clase. Ahora imaginemos que 2 instancias existen y que se refieren la una a la otra. Si todas las otras referencias de estos 2 objetos fueran removidas, entonces incluso cada objeto mantendría una referencia válida, no habría manera para cualquier thread vivo de acceder a estos objetos. Cuando el GC se ejecuta, puede usualmente descubrir cualquier isla de objetos y removerlos. Como podemos imaginar, tales islas pueden ser grandes, teóricamente conteniendo cientos de objetos. Examinemos el siguiente código:

 
	public class Island { 
		Island i; 
		public static void main (String[] args){ 
			Island i2 = new Island(); 
			Island i3 = new Island(); 
			Island i4 = new Island(); 
			i2.i = i3; // i2 refiere a i3 
			i3.i = i4; // i3 refiere a i4
			i4.i = i2; // i4 refiere a i2 
			i2 = null; 
			i3 = null; 
			i4 = null; 
			// Otras operaciones 
		} 
	}
	

Cuando el código alcanza el comentario // Otras operaciones, los 3 objetos Island (previamente conocidos como i2, i3 y i4) tienen variables de referencia que se refieren el uno al otro, pero para el mundo exterior han sido establecidos a null. Estos 3 objetos son elegibles para el GC.

Forzar la Recolección de Basura o el Garbage Collection

Lo primero que deberíamos mencionar aquí es que, al contrario que al título de lasección, el GC no puede ser forzado. Sin embargo, Java provee algunos métodos que permiten solicitar a la JVM a que realice un GC.

El GC ha evolucionado hasta un estado avanzado en el que es recomendable que nunca invoquemos System.gc() en nuestro código, dejemos esto a la JVM.

En realidad, es posible solo sugerir a la JVM que realice un GC. Sin embargo, no hay garantías de que la JVM actualmente elimine todos los objetos que no se usen de la memoria.

Las rutinas del GC que Java provee son miembros de la clase Runtime. La clase Runtime es una clase especial que tiene un objeto singular (Singleton) para cada programa main. El objeto Runtime provee de un mecanismo para comunicarse directamente con la máquina virtual. Para obtener la instancia de Runtime, podemos usar el método Runtime.getRuntime(), el cual nos retorna el Singleton. Una vez que tenemos el Singleton podemos invocar el GC usando el método gc().

Alternativamente, podemos llamar al mismo método en la clase System, el cual tiene métodos static que pueden hacer el trabajo de obtener el Singleton por nosotros. La manera mas simple de preguntar al GC es:

 
	System.gc();
	

Teóricamente, despues de llamar a System.gc(), obtendremos la mayor memoría libre como nos sea posible. Decimos teóricamente porque esta rutina no siempre trabaja de esta manera. Primero, nuestra JVM puede no implementar esta rutina; la especificación del lenguaje permite a esta rutina de no hacer nada en absoluto. Lo segundo, otro thread puede coger cierta memoria después de ejecutar el GC.

Esto no quiere decir que System.gc() sea un método que no sirva de nada (Es mejor que nada). Simplemente no podemos confiar en que System.gc() libere la suficiente memoria para que no nos tengamos que preocupar de no quedarnos sin ella.

Ahora que estamos algo mas familiarizados con el como funciona, vamos a ver un pequeño experimento para ver si podemos ver los efectos del GC. El siguiente programa nos permite saber de cuanta memoria dispone la JVM y cuanta memoría libre tiene. Entonces, crearemos 10.000 objetos Date. Despues de esto, nos dirá cuanta memoría queda libre y entonces llamaremos al GC (El cual, decidirá si se ejecuta o no). La memoria libre final resultante debería indicar cuando se ha ejecutado. Vamos a ver el programa:

 
	public class CheckGC { 
		public static void main (String[] args){
			Runtime rt = Runtime.getRuntime();
			System.out.println("Memoria Total de la JVM: " +
			rt.totalMemory()); System.out.println("Memoria Antes: " + rt.freeMemory()); 
			Date d = null; 
			for (int i = 0; i < 10000; i++){
				d = new Date(); 
				d = null; 
			} 
			System.out.println("Memoria Despues: " + rt.freeMemory()); 
			rt.gc();
			System.out.println("Despues del GC: " + rt.freeMemory()); 
		} 
	} 
	

Como podemos ver si ejecutamos el programa, la JVM decidió cuando usar el GC para los objetos elegibles. En el ejemplo anterior, le sugerimos a la JVM a realizar un GC cuando terminamos el bucle, y nos honra con nuestra petición. Este programa solo tiene un thread siendo ejecutado, por lo que no había nada mas ejecutandose cuando llamamos al método rt.gc(). Tenemos que tener en mente que el comportamiento de cuando el gc() es llamado puede ser diferente según las JVM, por lo que no hay garantías de que los objetos sin uso sean removidos de la memoria. Lo único que podemos garantizar es que si hay poca memoria, el GC se ejecutará antes de que se lance la excepción OutOfMemoryException.


Hasta aquí una pequeña introducción sobre el GC en Java, el cual debemos comprender su uso para futuros proyectos, y adelantarnos un poco a cuando se usará para que nunca nos exploten los programas.

Sin mas, cualquier aporte o corrección son bienvenidos.

Saludos!!!