Gestión de la memoria en Java

La JVM (máquina virtual Java).

La maquina virtual de java -o JVM en sus siglas en inglés- es el anclaje “físico” de nuestro maravilloso lenguaje multiplataforma con el Sistema Operativo correspondiente y, como tal, ocupa y utiliza memoria del sistema. Bien, esto es evidente pero, lo que es menos conocido, es como está implementada esta gestión de memoria.

La JVM se arranca reservando, por defecto, 64MB de memoria para trabajar que va ampliando según necesidades. Sin embargo, estas sucesivas ampliaciones automáticas llegan a un límite. Podemos gestionar nosotros mismos estos límites -tanto por arriba como por abajo- gracias a unos parámetros con los que se puede acompañar al comando de arranque de la JVM.

Esto es IMPORTANTE si sabemos que nuestra aplicación va a requerir una gran cantidad de memoria para su funcionamiento y queremos evitar un OutOfMemoryError o si, por el contrario, nuestra aplicación compite con otras de mayor prioridad en un sistema y queremos que utilice solo una parcela de memoria.

PARÁMETROS DE GESTIÓN DE MEMORIA

Antes de nada, es importante tener en cuenta que los parámetros de gestión de memoria son, en la mayoría de los casos, específicos de la maquina virtual. Así, el parámetro -Xlp es exclusivo de la maquina virtual implementada por IBM y se puede utilizar exclusivamente con esta.

Aunque hay un montón de implementaciones de JVMs, en la práctica, solo 3 o 4 son utilizadas en un porcentaje significativo y, en cualquier caso, siempre tendremos que probar nuestra aplicación con la implementación de referencia, la de SUN, para evitar posibles problemas de compatibilidades. Por eso, nos centraremos en los parámetros que se puedan utilizar con las maquinas virtuales de SUN.

Los parámetros de gestión de memoria se identifican por un sistema de prefijos que indican la naturaleza de dichos parámetros, así y según SUN:

* Los parámetros que comienzan con -X no son estándar, no se garantiza su implementación en todas las implementaciones de la JVM y pueden cambiar en futuras versiones del JDK.

* Los parámetros que comienzan con -XX no se consideran estables (¿¿??) y no se recomienda su uso para usuarios sin un conocimiento especifico.

Los principales parámetros de gestión de memoria son:

* Xms < tamaño > establece la cantidad inicial de memoria
* Xmx < tamaño > establece la cantidad máxima de memoria
* Xss < tamaño > establece el tamaño máximo de pila de cada hilo JAVA

Es decir, si quisiéramos arrancar nuestra aplicación con una cantidad de memoria asignada de 64MB y queremos que llegue a utilizar un GB, por ejemplo, tendríamos que lanzar un script de la siguiente manera:

java -Xms64m -Xmx1024m «nombre de nuestra aplicación»

Es importante conocer en profundidad que implican y para que se utilizan cada uno de estos parámetros:

* Los parámetros Xms y Xmx establecen el tamaño inicial y máximo de la cantidad de memoria que la JVM utilizará para ejecutar programas java pero, esto no implica que nuestro proceso java ocupe en memoria un tamaño máximo como el establecido en el parámetro Xmx. Como se ha especificado antes, la JVM es un proceso en si mismo y, como tal, necesita memoria para poder cargar su código y datos. Por tanto, un proceso java puede llegar a ocupar mas espacio en memoria que el determinado en Xmx.
* El parámetro Xss sirve para determinar cual es la cantidad de memoria máxima que podrá utilizar un hilo ejecutado dentro de la maquina virtual. Para aquellos que no estén familiarizados con la gestión de hilos, pondremos un ejemplo de sencilla comprensión: imaginemos una aplicación web desarrollada en java. Esta aplicación web tendrá sus datos comunes y ocupará X espacio en memoria. Cada vez que alguien haga una petición a nuestra página web, se creará un hilo de ejecución independiente que tendrá sus propios datos (por ejemplo, los datos de un formulario de registro en nuestra página) y que no serán compartidos por el resto de hilos que se estén ejecutando en dicho momento (nuestra página web no es monousuario puede tener un montón de usuarios concurrentes). Bueno, en este caso, toda la memoria que utilice cada uno de los hilos se suma para el cómputo de memoria global determinado por el parámetro Xmx pero, además cada hilo comprueba que no supere el máximo de memoria que determina el parámetro Xss. Si se supera, se lanza un StackOutOfMemoryError.

Un caso común y conocido donde se puede dar un StackOutOfMemoryError es a la hora de usar reflexión en procesos XSLT por lo que, probablemente tengamos que subir el tamaño máximo de pila por hilo si utilizamos este tipo de tecnología.

PARÁMETROS AVANZADOS DE GESTIÓN DE MEMORIA

Hasta ahora, hemos visto los parámetros más básicos de gestión de memoria pero hay muchísimos más. Vamos a centrarnos en dos de los más importantes o, lo que es lo mismo, de los que pueden solucionarnos problemas tanto de rendimiento como de funcionamiento (seamos sinceros, nadie se interesa por esto hasta que el marrón le estalla entre las manos).

El problema es que, antes de poder especificar los comandos, tendremos que tragarnos algo de literatura para comprender de que estamos hablando.

EL GARBAGE COLLECTOR

Toda máquina virtual viene con un recolector de basura o Garbage Collector. Este maravilloso invento es el que consigue que los programadores java nos olvidemos por completo de tener que lidiar con la gestión de la memoria (y es el culpable, por otro lado, que todo este tema nos suene tan raro y distante… y de ahí este articulo).

El Garbage Collector libera el espacio de memoria ocupado por objetos que no van a volver a ser utilizados. El proceso de localización y eliminado de esos objetos puede ser tan pesado que llegue a parar una aplicación hasta su conclusión, por lo que es especialmente importante que lo configuremos correctamente.

La JVM divide el espacio en “generaciones” o particiones en los que poder aplicar sofisticados algoritmos de recolección. Cuando alguno de estos espacios se llena, se fuerza una recolección de objetos y, la eficiencia de este sistema se basa en que la mayoría de los objetos tienen una vida útil muuuy corta. El espacio en memoria, se divide generalmente en dos particiones: la nueva y la vieja generación.

Cuando el tamaño de la nueva generación se llena, se invoca una recolección de objetos muy rápida (GC) que acaba con los objetos que no vayan a ser utilizados y que traspasa a la vieja generación los objetos con vida. Cuando la vieja generación se llena, se invoca una recolección completa (Full GC) que implica a toda la memoria y que es mucho más lenta que la recolección rápida. Para entender un poco mas gráficamente de lo que estamos hablando, veamos unas trazas de una JVM:

[GC 50650K->21808K(76868K), 0.0478645 secs]
[GC 51197K->22305K(76868K), 0.0478645 secs]
[GC 52293K->23867K(76868K), 0.0478645 secs]
[Full GC 52970K->1690K(76868K), 0.54789968 secs]

En estas trazas se puede ver cómo, tras la primera recolección rápida (GC), se pasó de 50650Kb usados a 21808Kb (de un total de 76868Kb que es el espacio asignado mediante -Xmx). También se puede ver cómo, tras la recolección completa (Full GC) se pasó a un uso de tan solo 1690Kb pero, a cambio de casi medio segundo de inactividad de nuestra aplicación, que es lo que tardo la recolección completa en llevarse a cabo.

Por tanto, no parece tener mucho sentido que tengamos un espacio de 4GB asignado a nuestro proceso java y un espacio de 2MB -que es lo que viene asignado por defecto- para nuestra nueva generación. Normalmente, se recomienda que pongamos el tamaño de la nueva generación que queramos mientras no supere el tamo determinado en -Xmx y, como buena práctica, se recomienda asignar la mitad del espacio total de memoria (Ej. java -Xmx1024m -Xmn512m «nombre de nuestra aplicación»). Así:

* Xmn < tamaño > establece la memoria asignada para la nueva generación.

También se puede forzar la recolección mediante una llamada al sistema en java con System.gc() pero, no se recomiendo su uso porque fuerza recolecciones completas e impide la escalabilidad de los grandes sistemas.

LA MEMORIA PERMANENTE

Un concepto que, a veces, no queda muy claro es que todo esto de lo que estamos hablando se refiere a memoria volátil, es decir memoria ocupada por datos con una esperanza de vida mas o menos larga pero, en ningún caso, pensados para ser utilizados durante todo el tiempo de ejecución de nuestro proceso. Sin embargo, si hay datos permanentes que utilizará nuestro proceso java.

Estos datos (classes, librerías, etc.) se almacenan en la memoria permanente que funciona adicionalmente al espacio establecido por nosotros para la memoria volátil mediante el parámetro -Xmx.

¿Que para que se utiliza esto realmente? Para la carga y descarga de classes dinamicamente y, cuando alguien se pregunte que clases son esas que se cargan dinamicamente debe recordar que nuestras JSPs se convierten en classes dinamicamente según se van invocando (aunque se pueda efectuar una precompilación para ganar en rendimiento) y que las librerías invocadas por nuestras aplicaciones también utilizan este espacio.

¿Cómo puede afectarnos esto? Bueno, la JVM establece por defecto un tamaño máximo de 64mb para la memoria permanente. Si nuestra aplicación empieza a generar clases como si no hubiera mañana (nadie duda de que todos nosotros hace aplicaciones con TRILLONES de usuarios concurrentes) y llenamos este espacio en memoria, forzaremos una recolección completa del Garbage Collector que penalizará el rendimiento de nuestra aplicación. En un escenario más real, si nos hemos dedicado a agregar y usar librerías y librerías con sus dependencias y nuestra memoria permanente no puede cargar todas las que necesitamos, daremos con un OutOfMemoryError que nos romperá el Chi a nosotros y a nuestros usuarios (no te digo ya a nuestro jefe). No parece que tenga muchos sentido, por otro lado, que le demos a nuestra mega-aplicación 2GB de memoria volátil y la dejemos con 64mb para cargar clases y librerías ¿verdad?. Así:

* XX:PermSize=< tamaño > establece el tamaño mínimo de la memoria permanente
* XX:MaxPermSize=< tamaño > establece el tamaño máximo de la memoria permanente

Si, por ejemplo, queremos utilizar 128mb para nuestra memoria permanente, ejecutaremos algo como java -Xmx1024m -Xmn512m -XX:PermSize=128m «nombre de nuestra aplicación»)

Es importante recalcar que la memoria permanente es adicional a la memoria volátil establecida por el comando -Xmx. Por eso, si arrancamos un proceso java con un permSize de 256mb y un -Xms de 256mb, el espacio total de memoria usada será de mas de 512mb (256 de memoria volátil + 256 de memoria permanente + la memoria que utilice la maquina virtual en si) Ej.


$ java -Xms256m -Xmx256m -XX:PermSize=256m -XX:MaxPermSize=256m Hello
$ pmap 6472
6472: /usr/java1.3.1/bin/../bin/sparc/native_threads/java -Xms256m
-Xmx256m
total 550544K


OPTIMIZACIÓN DEL RENDIMIENTO EN ARRANQUE Y EN TIEMPO DE EJECUCIÓN

Otra optimización que hay que tener en cuenta es la proporcionada por los diferentes perfiles de la maquina virtual y el uso del compilador de cada uno de los mismos. Así, el perfil por defecto es el de -client que prioriza la velocidad de arranque del proceso antes que el rendimiento general de la aplicación. Por el contrario, el perfil -server prima el rendimiento en tiempo de ejecución en vez que el arranque rápido del proceso (además de modificar ciertos valores por defecto como, por ejemplo, el tamaño por defecto de la memoria permanente que pasa de 32 a 64mb).

Para utilizar el perfil -server solo hay que introducirlo como parámetro en la ejecución de nuestro proceso java.

CONCLUSIONES

Por último, además de la correcta configuración de nuestra maquina virtual, otra de las cosas que debemos tener en cuenta es seguir una serie de buenas practicas a la hora de programar nuestras aplicaciones aunque dejaremos esto para otro post. Se pueden encontrar múltiples sitios con listas de optimización de memoria para nuestras aplicaciones pero destaquemos un articulo reciente sobre antipatrones de memoria que hay que evitar y que puede enseñarnos mucho.

También es importante tener en cuenta el enorme muestrario de aplicaciones de profiling y monitorización disponibles en el mercado, gratuitas y de pago, para comprender que pasa con nuestra memoria, cuando y porqué.

BIBLIOGRAFÍA

Artículo publicado bajo licencia CC - Some rights reserved en el blog de David Bonilla: http://sixservix.com/blog/david/2009/08/21/gestion-de-memoria-en-java/

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License