La memoria en Java, Garbage Collector y el método finalize()

A quien haya programado en lenguajes de programación tales como C, C++, o cualquier otro que permita un acceso real a la memoria, cuando se ha pasado a Java se habrá extrañado al no tener que ocuparse de la gestión de los recursos. Los conocidos métodos de C ‘malloc’ y ‘calloc’ pasan al olvido cuando programamos en este nuevo lenguaje, lo cual puede resultar cómodo, pero también puede generar ciertas dudas sobre quién y de que modo toma las decisiones de como se han de administrar los recursos de la memoria.

Antes de comenzar vamos a aclarar unos pequeños conceptos. La memoria está dividida en tres secciones diferentes, la Zona de Datos, Stack y Heap.

ZONA DE DATOS

Es donde se almacenan las instrucciones del programa, las clases con sus métodos y constantes (menos los finals). Esta zona de memoria es fija, y no se puede modificar durante el tiempo de ejecución.

STACK

El tamaño del Stack se define durante el tiempo de compilación y es estático durante su ejecución, por lo que puede llegar un momento en el que lo llenásemos y obtuviésemos un bonito StackOverflow que en java se representa mediante un ‘OutOfMemoryException’ . Es raro que nos encontremos con ello, pero si ejecutamos un método recursivo mal formado, es uno de los errores más comunes.

Los datos que se almacenan aquí son las referencias a objetos (instancias de objetos) y los datos primitivos como int, float o char. Cuando ejecutamos un método con variables locales, estas se cargan en el Stack y se eliminan una vez se finaliza el método.

El siguiente código generaría un StackOverflow, se trata de un ‘for’ infinito en modo recursivo.


public void meLlamoYoMismo(){

    meLlamoYoMismo();

}

HEAP

El Heap es la zona de memoria dinámica, almacena los objetos que se crean, en un principio tiene un tamaño fijo asignado por la JVM (Java Virtual Machine), pero según es necesario se va añadiendo más espacio.

STACK Y HEAP

Por lo que puede deducirse de las definiciones, el Stack y el Heap están estrechamente relacionados, ya que los objetos a los que apuntan las referencias almacenadas en el Stack se habrán creado en el Heap.


class MiClase{

    public static void main(String[] args){

        MiClase miObjeto;
        miObjeto = new MiClase();

    }

}

En la quinta linea se crea la referencia ‘miObjeto’ en el Stack y en la sexta se crea el objeto ‘new MiClase’ en el Heap y se enlaza ‘miObjeto’  a ella. Si la referencia ‘miObjeto’ apunta a otro objeto que se asigne en otro momento, el enlace anterior quedará roto y el objeto que se encuentra en el Heap no será enlazado por nadie ( Está consumiendo espacio en la memoria y no se hace uso de él).

Tiene que quedar claro que la relación referencia-objeto es de n-1, lo que quiere decir que un objeto puede ser apuntado por muchas referencias, pero que una referencia apunta a un solo objeto.

Otro ejemplo un poco más complejo que ilustra lo mismo que el anterior.


class MiClase{

    public static void main(String[] args){

        MiClase miObjeto;
        MiClase miOtroObjeto;
        miObjeto = new MiClase();
        miOtroObjeto = miObjeto;

        miOtroObjeto = new MiClase();

        miObjeto = null;
        miOtroObjeto= null;

    }

}

En las líneas cinco y seis se crean dos referencias de tipo ‘MiClase’ con los nombre ‘miObjeto’ y ‘miOtroObjeto’, en la siguiente se crea un objeto y se le asigna a la referencia ‘miObjeto’. En la línea ocho, tenemos ya dos referencias a ‘new MiClase()’, esto es, dos referencias a una misma instancia de objeto.

En la décima creamos otra nuevo objeto al que hace referencia ‘miOtroObjeto’, por lo que ya tenemos dos referencias a dos objetos diferentes. Las dos últimas lineas rompen esos enlaces al ser asignadas ambas referencias a null.

GARBAGE COLLECTOR

El Garbage Collector es un proceso de baja prioridad que se ejecuta en la JVM y es el encargado de liberar la memoria que no se emplea. El ser de baja prioridad supone que no pueda estar todo el rato trabajando, y que solo se le asigne su tarea cuando el procesador no tiene un trabajo con mayor prioridad en ejecución.

¿Cómo sabe el Garbage Collector lo que puede borrar y lo que no?  Es algo muy simple, si un objeto no tiene referencias desde el Stack tiene que ser eliminado.

La magia de este recolector de basura deja asombrados a los programadores que odian tener que gestionar la memoria ellos mismos, y es que es algo muy útil, pero se trata de un arma de doble filo. Entre sus contras tenemos que al tratarse de un proceso de prioridad baja, es poco probable que se ejecute cuando se esté haciendo un uso intensivo de la CPU. Esto se puede solucionar si se solicita una pasada del Garbage Collector desde el propio código. No se tiene que abusar de ello, pero puede resultar interesante tras una serie de operaciones que se sepa a ciencia cierta que puede dejar objetos sin referencias. El método de solicitar esta pasada se muestra en las siguientes lineas.

public void pasarGarbageCollector(){

    Runtime garbage = Runtime.getRuntime();
    garbage.gc();

}

El código que se muestra a continuación es un método de probar que esto efectivamente ayuda a liberar la memoria. Se realiza un bucle en el que se generan referencias y se eliminan las mismas y al terminar se solicita a la JVM que elimine todo objeto no referenciado. También se muestran por consola la memoria libre que se tenía antes y después de la petición.

class TestRecolector{

    public static void main(String[] args){

        TestRecolector test = new TestRecolector();
        test.testear();

    }
    public void testear(){
        Date fecha = null;
        for (int i = 0; i<99999999;i++){
            fecha = new Date(2011,8,7);
            fecha = null;
        }

        this.pasarGarbageCollector();
    }
    public void pasarGarbageCollector(){

        Runtime garbage = Runtime.getRuntime();
        System.out.println("Memoria libre antes de limpieza: "+ garbage.freememory());

        garbage.gc();

        System.out.println("Memoria libre tras la limpieza: "+ garbage.freememory());
    }
}

Antes de continuar, mientras tenemos todo esto aun fresco, un consejo para programadores. En muchas ocasiones, al hacer operaciones, especialmente al trabajar con ‘ArrayList’ u otras listas, acostumbramos a emplear una variable temporal que solo resulta útil durante el tiempo de ejecución de esa operación. Cuando estas operaciones se hacen en un método externo al hilo principal no hay problema, pues sus “residuos” serán limpiados al salir del método, pero si por alguna razón se realizan en el hilo principal, o en un método que reúna múltiples operaciones (y que está largo rato ejecutándose), sería una buena idea que al terminar con el bucle se le asignara el valor ‘null’ a esas variables para dejar claro al Garbage Collector que ya no nos interesa mantenerla en memoria.

EL MÉTODO finalize()

El otro problema que supone el paso automático o descontrolado del Garbage Collector es que puede eliminar un objetos con información que queríamos guardar y que igual se ha quedado sin referencia por culpa nuestra. Por suerte, todas las clases heredan de ‘Object’ y con ello el método ‘finalize()’. Antes de que la JVM decida eliminar un objeto perdido, ejecuta este método. Gracias a ello podemos por ejemplo trabajar con una serie de elementos, eliminar la referencia a los mismos y no preocuparnos de guardar los datos esenciales en una base de datos (siempre que se reimplemente finalize() en ese objeto).

Dejo un método ilustrativo muy simple que muestra el funcionamiento de finalize();


public class Finalizando{

    String nombre;

    Finalizando (String pNombre){

        this.nombre = pNombre;

    }

    protected void finalize(){
        System.out.println(this.nombre);
    }
    public void pasarGarbageCollector(){

        Runtime garbage = Runtime.getRuntime();
        garbage.gc();
    }
    public static void main (String[] args){

        Finalizando persona = new Finalizando("Marta");
        persona = null;
        this.pasarGarbageCollector();

    }
}

El resultado de esto sería imprimir por consola “Marta”.
El ejemplo es algo banal, pero imaginemos que tenemos un ‘ArrayList<Persona>’ y que queremos recorrerlo mediante un while empleando un ‘miArray.remove(0)’ a fin de no tener que manejar un contador y sabiendo que no se emplearán de nuevo esos objetos una vez sus datos han sido almacenados en una base de datos. Para ello, pese a que no lo recomiende, puede implementarse el método ‘finalize()’ y despreocuparse uno de ello a posteriori.  Una cosa a tener en cuenta si se decide hacer esto, es que que nosotros dejemos sin referencia antes a un objeto que a otro, no implica que el Garbage Collector ejecute antes el ‘finalize()’ de ese objeto.

Espero que con esto tengáis más claro como se realiza la gestión de memoria en un entorno Java.

Un saludo y hasta pronto.

Diferencias entre IdentityHashMap y HashMap

Los HashMap e IdentityHashMap se emplean para almacenar una serie de valores u objetos accesibles a través de su clave.

Durante unos cuantos años he trabajado con estas dos clases indistintamente en mis trabajos de clase y no recuerdo que alguien me comunicase que existían diferencias notables entre ellas. Supongo que a otros muchos les pasa lo mismo y se acostumbran al uso de una de ellas empleándola para todo hasta que un día un programa no funciona como se esperaba y no aparece el error por ningún lado.

La gran diferencia entre estas dos clases es como realizan la comparación de la clave o ‘key’ cuando se quiere acceder o insertar un dato almacenado en el Map. IdentityHashMap emplea el operador ‘==’ mientras que HashMap hace uso del método ‘equals()’.

Para quien no recuerde sus primeras clases de programación Java, aclarar que el operador ‘==’ comprueba las referencias de los objetos por lo que ante el siguiente código en una función que devuelva un valor boolean el resultado sería ‘true’. Ya que tanto ‘a’ como ‘b’ hacen referencia al mismo String “hola”.

String a=”hola”;
String b=”hola”;
return a==b;

En este otro caso sin embargo el resultado sería ‘false’ pues ‘a’ tiene la referencia a “hola” mientras que ‘b’ a un nuevo String cuyo contenido es “hola”.

String a=”hola”;
String b=new String(“hola”);
return a==b;

El método ‘equals()’ empleado por los HashMap, comprueba que los valores de los dos objetos a igualar tengan el mismo valor. En ambos ejemplos el resultado sería ‘true’ en este caso.

String a=”hola”;
String b=”hola”;
return a.equals(b);
String a=”hola”;
String b=new String(“hola”);
return a.equals(b);

Una vez aclarados estos conceptos básicos veamos como afecta esto a la hora de introducir un nuevo valor junto a su ‘key’ tanto en los HashMap como en los IdentityHashMap.


import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
public class IdentityMap {public static void main(String[] args) {
    Map identityHashMap = new IdentityHashMap();
    Map hashMap = new HashMap();identityHashMap.put("a", 1);
    identityHashMap.put(new String("a"), 2);
    identityHashMap.put("a", 3);hashMap.put("a", 1);
    hashMap.put(new String("a"), 2);
    hashMap.put("a", 3);

    System.out.println("Número de elementos IdentityHashMap: " + identityHashMap.keySet().size());
    System.out.println("Número de elementos HashMap" + hashMap.keySet().size());

 }

El resultado sería:

Número de elementos IdentityHashMap: 2
Número de elementos HashMap: 1

Esto sucede ya que cuando en el IdentityHashMap introducimos dos valores mediante la clave “a”, esta es tomada como la misma entrada guardándose solo una, mientras que el ‘new String(“a”)’ representa un nuevo String y por lo tanto una nueva referencia. Por otro lado en el HashMap, al examinarse solamente el valor, e ignorarse la referencia entre ellos, los tres valores son iguales.

Tras todo esto, espero que tengáis más claro cuando usar el IdentityHashMap y cuando un HashMap, ya que  en proyectos complejos esta decisión puede acarrear serios problemas difíciles de encontrar.