domingo, 1 de febrero de 2009

Depurar fugas de memoria en Java (Tomcat)

Hola a todos. Por fin logro sacar algo de tiempo para escribir el primer post de este año. Esta vez está relacionado con la depuración de bugs relacionado con la memoria.

Si tienes problemas de memoria con Tomcat, o en general con aplicaciones Java(OutOfMemoryException), necesitas o simplemente tienes curiosidad de saber la distribución de la memoria de tu aplicación, no sigas buscando, este es tu post.

Las opciones que voy a mostrar son opciones de configuración de la Máquina Virtual Java (JVM), por lo tanto son aplicables tanto a una simple clase con un método main, como a una aplicación sobre Tomcat o cualquier otro servidor de aplicaciones. Sin embargo, en este post voy a tomar como ejemplo Tomcat, ya que es lo que uso en el desarrollo de aplicaciones web.

Más concretamente utilicé estas opciones para investigar un problema que teníamos con Tomcat. Cada cierto intervalo regular de tiempo, el Tomcat se caía debido a un error de OutOfMemory. Por más que aumentamos la memoria, seguía cayéndose aunque a intervalos más grandes, por lo que empezamos a sospechar que había algo en nuestra aplicación que iba consumiendo memoria y no la liberaba. Nuestra aplicación tenía fugas de memoria.

Para resolverlo necesitabamos saber dónde estaba el problema, y al igual que ocurre cuando te rompes un hueso, el mejor diagnóstico se hace si tienes una radiografía de la memoria. Por suerte Java nos permite hacer esto. Para ello sólo hay que usar el comando jmap.

Antes de continuar comentar que todo lo que expongo aquí es para Java 6. Con Java 5 también lo podéis hacer a partir de cierta versión, pero el formato de los comandos cambia un poco. De todas formas, mi recomendación es que paséis a Java 6. El paso es casi inmediato y os ahorraréis muchos quebraderos de cabeza con estos comandos y opciones.

Para hacer un volcado de la memoria del Tomcat, tendréis que ejecutar lo siguiente:

bash$ jmap -dump:file=heap.hprof <pid>

donde <pid> es el id del proceso Tomcat. Podéis sacarlo con el comando ps:

bash$ ps -ef | grep tomcat
usuario 13832 1 79 22:37 pts/0 00:00:14 /usr/lib/jvm/java-6-sun/bin/java -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.util.logging.config.file=/opt/tomcat/conf/logging.properties -Djava.endorsed.dirs=/opt/tomcat/common/endorsed -classpath :/opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/commons-logging-api.jar -Dcatalina.base=/opt/tomcat -Dcatalina.home=/opt/tomcat -Djava.io.tmpdir=/opt/tomcat/temp org.apache.catalina.startup.Bootstrap start

En mi caso el pid sería 13832 y por tanto la ejecución del comando jmap quedaría así:

bash$ jmap -dump:file=heap.hprof 13832
Dumping heap to /opt/apache-tomcat-5.5.25/heap.hprof ...
Heap dump file created

Esto genera el fichero heap.hprof en el directorio donde ejecutéis el jmap. Este fichero es un volcado de lo que hay en la memoria del Tomcat, y por tanto ocupará tanto como la memoria del Tomcat. Comprobad que tenéis espacio en disco suficiente para generarlo.

Sin embargo, no necesitábamos una radiografía de la memoria en cualquier momento, la necesitábamos justo en el momento en que el hueso se rompía, es decir, cuando el Tomcat lanza la OutOfMemoryException. Por suerte, contamos con las opciones de la JVM. Para que Tomcat -y repito: Tomcat o cualquier aplicación Java- genere un volcado de la memoria al producir una OutOfMemoryException tenéis que poner entre sus JAVA_OPTS las siguientes opciones:

-XX:HeapDumpPath=/ruta/donde/guardar/el/fichero/ -XX:+HeapDumpOnOutOfMemoryError

Observad que es un más(+) lo que hay antes de HeapDumpOn..... Si ponéis un menos(-), no os funcionará. Es un error muy típico ;-)

En el caso del Tomcat, tendremos que editar el fichero catalina.sh y meter las opciones en la variable CATALINA_OPTS. Por ejemplo:

CATALINA_OPTS="-XX:HeapDumpPath=/tmp/ -XX:+HeapDumpOnOutOfMemoryError [...resto de opciones...]"

Un Tomcat funcionando con estas opciones generará un fichero en /tmp llamado heap<pid>.hprof automáticamente cuando se produzca una OutOfMemoryException. Más información sobre esas opciones en Java HotSpot VM Options.

Una vez configurado de esta forma, ya sólo teníamos que esperar a que el Tomcat volviese a fallar. Y de eso estábamos seguros. Una vez con el fichero hprof generado podemos usar dos herramientas para procesarlo.

Una es VisualVM. Es de Sun y viene con el JDK de Java 6. Es el comando jvisualvm que se encuentra en el directorio bin del JDK. Con esta herramienta puedes navegar por los objetos que hay en memoria, ver las clases que más instancias tienen y que más consumen, etc. Te muestra lo que hay en memoria literalmente, pero para mi gusto es demasiada información en bruto y quizás sea complicado ver lo que está pasando.

La otra herramienta es Eclipse Memory Analyzer. Esta herramienta procesa la información y saca gráficas y estadísticas útiles. Incluso te propone posibles errores. Con esta herramienta fue con la que rápidamente descubrimos el error, ya que la gráfica de tarta de la ocupación de la memoria estaba al 75% ocupada por cierta parte de la aplicación. Se trataba de una de nuestras cachés. De ahí, muy hábiles nosotros, dedujimos que el proceso de limpiado de esa caché no estaba funcionando correctamente y por tanto, esta caché era un saco donde se metían cosas, pero nunca salían. :-p

Espero que esto os ayude a resolver este tipo de problemas si es que lo tenéis, y si no tienes problemas de este tipo, ¿nunca te has preguntado cuanto ocupa tu aplicación en memoria y qué objetos ocupan más? Merece la pena probar.

12 comentarios:

Emilio Escobar dijo...

Un detalle para el Eclipse Memory Analyzer. Normalmente será necesario parsear el dump desde la consola ejecutando,

MemoryAnalyzer -consolelog -application org.eclipse.mat.api.parse /path/al/dump.hprof

Xela dijo...

Esto que comenta Emilio es útil cuando se tienen archivos grandes. La prueba que he hecho ha ido sin problemas ya que tan sólo ocupaba unos 25Mb, pero si tenéis heaps de algún Giga, es posible que el entorno gráfico no sea capaz de parsearlo y tengáis que usar la consola.

Buena apreciación, Emilio. ;-)

Anónimo dijo...

Con Tomcat 4.1 es posible hacer este debug?

Xela dijo...

Realmente no depende de la versión de Tomcat sino más bien de la versión de Java sobre la que se ejecuta el Tomcat. Recuerda que son opciones de la JVM y que pueden aplicarse a cualquier aplicación Java, ya sea Tomcat u otra.

Si no has parcheado el Tomcat 4, supongo que tendrás la versión de Java 1.4. Según la documentación de Java sobre las Java HotSpot VM Options, la opción -XX:+HeapDumpOnOutOfMemoryError fue añadida en la JVM 1.4 a partir de la versión 1.4.2 update 12. Por tanto, si tienes esa o superior supongo que podrás usarlo sin problemas.

Saludos

Anónimo dijo...

Como sulucionaste el problema de caché?

Xela dijo...

La cache la habíamos hecho nosotros y el fallo estaba en que no limpiaba los contenidos cacheados. El método para limpiar los contenidos antiguos estaba, pero por error no se ejecutaba nunca. Lo único que hacía era meter información en la memoria. Cuando llenaba la memoria, OutOfMemoryError.

Simplemente tuvimos que corregir la ejecución del método de limpieza de caché.

No sé si esto responde a tu pregunta. ;-)

Ivan dijo...

Hola Xela:

Tengo el problema similar, ya pude analizar donde esta el problema pero me sigue apareciendo lo mismo, al parecer no estoy limpiando el cache, cual es la manera correcta de limpiar el cache? me refiero a las instrucciones...

Espero me puedas ayudar. Gracias y saludos.

Anónimo dijo...

Muchas gracias, me ha sido de gran utilidad.

Un saludo!!

El IngeNery dijo...

tengo una duda lo explicaste perfecto para linux pero el detalle es que yo desarrollo en windows :( que puedo hacer en windows para obtener estos archivos

Alex Guerra (Xela) dijo...

Hola IngeNery, para windows debe ser más o menos igual. El jmap debe estar en el directorio bin de la instalación de Java. Y en lugar de editar el catalina.sh tendrás que editar el catalina.bat. ¿Qué problema tienes exactamente?

El IngeNery dijo...

gracias alex tu blog me sirvio como no tienes una idea no use el jmap use las opciones del la jvm genere el archivo y con el eclipse analyzer pude encontrar mi fuga de memoria, si esto fuera un foro como taringa y tuviera puntos te daria +10 jejeje...

Alex Guerra (Xela) dijo...

Muchas gracias IngeNery ;-)