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.

Anuncios

6 Respuestas a “La memoria en Java, Garbage Collector y el método finalize()

  1. Pingback: Asociación de Egresados del Instituto Tecnológico de Querétaro.

  2. yo tengo un problema al cargar datos de un fichero txt que almacena log de correos y nececito cargar estos datos a una base de datos mysql. La cuestion es que cuando va aproximadamente por 10000 lineas leidas me da el error:
    Exception in thread “AWT-EventQueue-0” java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOfRange(Arrays.java:3209)
    at java.lang.String.(String.java:216)
    at java.io.BufferedReader.readLine(BufferedReader.java:331)
    at java.io.BufferedReader.readLine(BufferedReader.java:362)

    he prebado colocando estas lineas:
    Runtime basurero = Runtime.getRuntime();
    basurero.gc();
    System.gc();
    System.runFinalization();
    finalize();

    dentro del while que utilizo, tanto cada cierto numero de iteraciones, y hasta he llegado a ejecutarlas en cada iteracion pero el error continua y mas menos cuando llega al mismo numero de lineas del fichero leidas eh insertadas en la BD.
    alguien tiene idea que es lo que ocurre?

  3. Gracias por la rapida respuesta Oscar, resulta que yo presisamente hago eso que me sugieres o al menos eso creo. Yo tengo una clase “lectura” que contiene una funcion “leertxt” la cual invoco desde mi principal pasandole la direccion del fichero. En esta funcion cargo el fichero conpleo y voy leyendo liena por linea y ese dato se la paso a otra funcion llamada “insertarDatos” a la que le paso los datos y la conexion que creo fuera del while.
    el codigo de la clase “lectura” es el siguiente:

    public class lectura {

    newprincipal princ = new newprincipal();

    @SuppressWarnings(“static-access”)
    public String leertxt(String args) {

    Funciones insert = new Funciones();
    File archivo = null;
    FileReader fr = null;
    BufferedReader br = null;
    String[] datos = null;
    String mensage = null;
    String linea = null;
    int contador = 0;

    try {

    Connection connection;
    Class.forName(“com.mysql.jdbc.Driver”);
    connection = DriverManager.getConnection(“jdbc:mysql://localhost/prueba”, “root”, “”);

    //Cargamos el archivo de la ruta relativa
    archivo = new File(args);
    //Cargamos el objeto FileReader
    fr = new FileReader(archivo);
    //Creamos un buffer de lectura
    br = new BufferedReader(fr);

    JOptionPane mensag = new JOptionPane();

    //Leemos hasta que se termine el archivo
    while ((linea = br.readLine()) != null) {

    //Utilizamos el separador para los datos
    datos = linea.split(” “);
    //Presentamos los datos
    System.out.println(“Usuario: ” + datos[2] + ” Dominiolocal: ” + datos[4] + ” Receptor: ” + datos[9] + ” Dominiolocal: ” + datos[10] + ” Peso: ” + datos[12]);

    try {

    insert.insertarDatos(datos[2], datos[4], datos[9],datos[10], datos[12], connection);
    contador++;

    if (contador == 3000 || contador == 3000 || contador == 6000 || contador == 9000 || contador == 12000 || contador == 15000)
    {
    Runtime basurero = Runtime.getRuntime();
    basurero.gc();
    System.gc();
    System.runFinalization();
    finalize();
    }
    //variable a incrementar

    }catch (ClassNotFoundException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
    } catch (SQLException e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
    } catch (Throwable e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }

    }

    mensag.showMessageDialog(null,”El fichero se cargo correctamente.”);
    mensage = “Fichero: ” + args + ” cargado.”;

    //Capturamos las posibles excepciones
    } catch (Exception e) {
    JOptionPane.showMessageDialog(null,”La dirección del fichero no es válida.”);
    mensage = “”;
    //e.printStackTrace();
    } finally {
    try {
    if (fr != null) {
    fr.close();
    }
    } catch (Exception e2) {
    e2.printStackTrace();
    }
    }

    return mensage;

    }

    @SuppressWarnings(“unused”)
    private Object getJContentPane() {
    // TODO Auto-generated method stub
    return null;
    }
    }

    y esta es la funcion insertarDatos que esta en otra clase “Funciones”.

    public static void insertarDatos(String usuario,String dominio_local,String receptor,String dominio_receptor, String peso, Connection con) throws ClassNotFoundException, SQLException{

    Runtime basurero = Runtime.getRuntime();
    basurero.gc();
    System.gc();
    System.runFinalization();

    String sqlSentenc =”insert into correos values(?,?,?,?,?,?)”;
    PreparedStatement prepararCons = con.prepareStatement(sqlSentenc);

    //…………..validando los Datos para eliminar las comillas…………………………

    String validarusuario = “”;
    for(int i=0;i<usuario.length() ;i++){

    if(usuario.charAt(i) != '"'){

    validarusuario += usuario.charAt(i);
    }
    }

    String validardominiolocal = "";
    for(int i=0;i<dominio_local.length() ;i++){

    if(dominio_local.charAt(i) != '"'){

    validardominiolocal += dominio_local.charAt(i);
    }
    }

    String validarreceptor = "";
    for(int i=0;i<receptor.length() ;i++){

    if(receptor.charAt(i) != '"'){

    validarreceptor += receptor.charAt(i);
    }
    }

    String validardominioreceptor = "";
    for(int i=0;i<dominio_receptor.length() ;i++){

    if(dominio_receptor.charAt(i) != '"'){

    validardominioreceptor += dominio_receptor.charAt(i);
    }
    }

    String validarpeso1 = "";
    for(int i=0;i<peso.length() ;i++){

    if(peso.charAt(i) != ','){

    validarpeso1 += peso.charAt(i);
    }
    }

    float validarpeso2 = Float.parseFloat(validarpeso1);

    prepararCons.setString(1,null);
    prepararCons.setString(2,validarusuario); // estamos dándole valor al primer parametro que se pasa, es decir al primer ? que aparezca.
    prepararCons.setString(3,validardominiolocal);
    prepararCons.setString(4,validarreceptor);
    prepararCons.setString(5,validardominioreceptor);
    prepararCons.setFloat(6,validarpeso2);

    prepararCons.execute();

    }

  4. Ya resolvi el problema, fui reduciendo los procesos hasta que me di cuenta que el bateo lo daba en esta linea:

    PreparedStatement prepararCons = con.prepareStatement(sqlSentenc);

    entonces simplemente prepare toda la conecion fuera del while y en vez de pasarle a la funcion “insertarDatos” la conexion, le pase el “PreparedStatement” ya elaborado. Y finalmente leyo todo el fichero sin dar error de memoria.

    bueno muchas gracias de todos modos por la ayuda prestada.
    Saludos desde Cuba.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s