Seguro que muchos de vosotros os acordáis (y otros muchos seguís utilizando) lenguajes como C, donde la responsabilidad de la gestión de la memoria de los programas recaía en el programador. En C, se deben utilizar las intrucciones malloc y calloc para asignar memoria dinámicamente y la función dispose para liberar la memoria que hemos asignado en nuestro programa para que pueda ser reutilizada.
Una de las características que incorporó el lenguaje Java es la gestión dinámica de la memoria. Con la programación en Java nos olvidamos de gestionar la memoria y toda la responsabilidad recae en el lenguaje. Se asignarán variables según se vayan creando en la memoria HEAP y se liberará automáticamente esta memoria cuando no se necesita más por un proceso de la máquina virtual de Java (JVM) llamado Garballe Collector (GC).
Tipos de Memoria
En Java podemos hablar de tres zonas de memoria:
- La ZONA DE DATOS, donde se almacenan las instrucciones del programa, las clases con sus métodos y constantes. Esta zona es inmutable durante todo el periodo de ejecución del programa.
- La memoria HEAP, donde se almacenan las variables que se crean en los programas con la sentencia "new". Esta zona de memoria es dinámica e irá creciendo conforme se vayan creando objetos desde nuestro programa.
- La memoria STACK, donde se almacenan los métodos y las variables locales que se crean dentro de los mismos. Esta zona de memoria es estática y no se modifica durante el desarrollo del programa. Cada vez que un método se llama se coloca al comienzo de la pila y se almacenará información como el número de la línea que se está ejecutando así como todos los valores de las variables locales. Los Threads o Hilos tienen su propia pila de llamadas. Un error que nos puede ocurrir es que al ejecutar nuestro programa obtengamos la excepción java.lang.OutOfMemoryException por un StackOverflow. La causa podría ser por ejemplo que estamos realizando una llamada recursiva y no hayamos implementado correctamente la condición de parada.
![]() |
Diferencias entre memoria STACK y HEAP (Imagen: prietopa) |
Ventajas y Desventajas de la Gestión Dinámica
Una desventaja de la gestión de memoria en Java es que nosotros no controlamos donde queremos asignar nuestra memoria. Asignar memoria en el HEAP es más costoso y también liberar dicha memoria, por eso, a veces es deseable almacenar un objeto en la memoria STACK.
Entre las ventajas de la gestión dinámica es que no tenemos que preocuparnos por asignar y desasignar memoria, puesto que es un proceso dinámico. Además la JVM internamente utiliza una optimización llamada análisis de escape (en inglés scape analysis) que realiza comprobaciones para saber si un objeto se utiliza sólo en un thread o un método y crearlo en la memoria STACK, incrementando el rendimiento de nuestros programas.
Garbage Collector
Como hemos mencionado anteriormente, el Garbage Collector o recolector de basura es un proceso automático de baja prioridad que se ejecuta dentro de la JVM. Se encarga de limpiar aquella memoria del HEAP que ya no se utiliza y por tanto, podría ser utilizada por otros programas. ¿Cómo sabe la memoria que no se utiliza? Un objeto podrá ser limpiado cuando desde el STACK ninguna variable haga referencia al mismo.
Si queremos saber cuando se ejecutar el GC deberemos utilizar el parámetro "-verbose:gc" antes de arrancar nuestra JVM.
El proceso GC también se puede ejecutar manualmente desde un programa Java si creemos que en un punto determinado hemos podido dejar muchos objetos sin referenciar. A continuación os muestro mediante un pequeño programa Java cómo se puede calcular la memoria libre en un momento determinado en la JVM y cómo ejecutar el Garbage Collector manualmente desde un programa:
package es.jpascu.test; import java.util.ArrayList; import java.util.List; public class TestJVMMemory { private static final long MEGABYTE = 1024L * 1024L; public static long bytesToMegabytes(long bytes) { return bytes / MEGABYTE; } public static void main(String[] args) {
- // Voy llenando memoria HEAP con objetos que luego no tendrán referencia.
- // Esta memoria será liberado por la GC de JAVA for (int i = 0; i <= 1000000; i++) { Item item = new Item("1", "UN MURCIANO EN EL POLO", 2); item = null; } // Obtenemos la runtime de Java Runtime runtime = Runtime.getRuntime(); // Ejecutamos el proceso de Garbage Collector runtime.gc(); // Calculamos la memoria máxima y la memoria disponible long memory = runtime.totalMemory() - runtime.freeMemory(); long memoriaMax = runtime.maxMemory(); System.out.println("Memoria máxima en bytes: " + memoriaMax); System.out.println("Memoria máxima en MB: " + bytesToMegabytes(memoriaMax)); System.out.println("Memoria utilizada en bytes: " + memory); System.out.println("Memoria utilizada en MB: " + bytesToMegabytes(memory)); } }
La JVM asigna el tamaño inicial y el tamaño máximo de la memoria HEAP cuando arranca pero nosotros podemos modificar esos parámetros si lo creemos oportuno. Por ejemplo, a veces nos podría dar una excepción java.lang.OutOfMemoryException en nuestro servidor J2EE (Tomcat, Weblogic...) porque una aplicación está consumiendo demasiada memoria y el tamaño que puede alcanzar la memoria HEAP es demasiado pequeño. También podremos modificar el tamaño máximo de la memoria HEAP que nunca deberá exceder el tamaño de la memoria física de la máquina.
Para indicar el tamaño mínimo que tiene la memoria HEAP utilizamos el parámetro:
- -Xms1024m. Establecemos el tamaño mínimo del HEAP en 1024 MB.
Para indicar el tamaño máximo que tiene la memoria HEAP utilizamos el parámetro:
- -Xmx1800m. Establecemos el tamaño máximo del HEAP en 1800MB.
Modificar tamaño de memoria HEAP en ECLIPSE |
Write once, run anywhere
Después de haber visto cómo se gestiona la memoria vamos a repasar un punto que a mi entender es muy interesante en la plataforma Java, la compilación y ejecución de los programas.
El título del apartado es un eslogan famoso para promocionar el lenguaje Java y hace referencia al soporte multiplataforma de Java. ¿Por qué? Porque en Java escribimos un programa una vez y lo compilamos a bytecodes. Este código compilado nos sirve para ejecutar el programa en diversas plataformas (móviles, Linux, Windows...). Para ello sólo necesitamos diferentes máquinas virtuales (JVMs) que traduzcan los bytecodes al código máquina de la plataforma correspondiente.
![]() |
Compilador JIT (Imagen: profejavaoramas) |
En las primeras versiones de Java la JVM era simplemente un interprete de bytecodes. Esto hacía que los programas se ejecutaran con lentitud, ya que se tenía que interpretar y ejecutar los bytecodes a la vez sin realizar ninguna optimización.
En las versiones actuales de la JVM se ejecutan los programas combinando la interpretación de bytecodes y la compilacion JIT o Just-In-Time. ¿Qué demonios es esto? Pues que la JVM analiza bytecodes a medida que se van interpretando e identifica los bytecodes que se ejecutan con más frecuencia (puntos activos o 'hot spots'). Estos bytecodes son traducidos a lenguaje máquina correspondiente por el compilador JIT. Cuando la JVM encuentre de nuevo estos puntos activos nuevamente, ejecutará directamente el código máquina disminuyendo el tiempo de ejecución del programa y aumentando el rendimiento.
Espero que el artículo os haya ayudado a entender mejor el mundo Java.
la parte donde esta Item item = new Item("1", "UN MURCIANO EN EL POLO", 2);
ResponderEliminarque significa
Hola Jose. Gracias por leer el artículo.
ResponderEliminarEl código que quería poner en realidad es para crear objetos y asignarlos a variables y posteriormente asignar esta variable a null para que quedaran sin referencia y por tanto, ir lleando la memoria.
Lo corrigo para que se vea mejor. Gracias y un saludo.