miércoles, 13 de mayo de 2009

Memory leaks en Java

Hace poco tuve un inconveniente desarrollando una aplicación web con Genexus/Java, la memoria que consumía el tomcat crecía hasta que daba un error java.lang.OutOfMemoryError: Java heap space.

El problema se daba durante la ejecución de un proceso batch muy largo y era claro que una vez que terminaba no liberaba la memoria que había utilizado, entonces ¿Como identificar que es lo que está quedando colgado?

Lo primero que usé fue JConsole, que una vez habilité JMX en el tomcat, me permitió ver el consumo de memoria global, como crecía y decrecía, pero seguía sin ver que objetos eran los que estaban quedando colgados.

Busqué varios profilers para Java y encontré que principalmente apuntan a mostrar los tiempos de ejecución de las operaciones, así que en este caso no me servían y me di cuenta que tenía que buscar alguna herramienta para analizar la memoria. Estoy seguro de que existen muchas y hay mejores soluciones que la que utilicé, pero lo que encontré sirvió a mis propósitos y tiene la ventaja y contra de no tener que estar analizando los datos en tiempo real.

Solución: JConsole + jhat (Java Heap Analysis Tool), ambas son herramientas que vienen con el JDK de SUN, simplemente hay que buscarlos en la carpeta bin.

jhat es un programa que dado un "memory dump" levanta un servidor web que permite navegar la memoria en dicho dump, además de presentar útiles histogramas de instancias y cantidad de memoria por clase. Pero hay que tener en cuenta que para un dump grande 100MB+ hay que pasarle parámetros para agrandar el heap del propio jhat, sino se queda sin memoria y hay que sentarse a esperar porque se toma su tiempo para procesar los datos.

Entonces los pasos a seguir son:
  1. Habilitar JMX en el Tomcat, agregar a la configuración los siguientes parámetros:
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=”9004″
    -Dcom.sun.management.jmxremote.authenticate=”false”
    -Dcom.sun.management.jmxremote.ssl=”false”
  2. Ejecutar los procesos que se sabe que dejan memoria colgada o usar la aplicación hasta que la memoria crezca considerablemente.
  3. Iniciar JConsole y conectarse al servidor en el puerto 9004.
  4. En el tab MBeans ejecutar com.sun.management.HotSoptDiagnostic.dump usando como parámetro "C:\dump.bin" (o la ruta que venga bien).
  5. Ejecutar jhat pasando como parámetro el archivo a procesar, opcionalmente se le puede agrandar la memoria de heap porque con archivos grandes puede caer.
    jhat -J-Xmx400m dump.bin
  6. Entrar a la url http://localhost:7000/ donde debería aparecer la página de bienvenida de jhat, con un resumen de las clases que hay en memoria y al final unos links a consultas preparadas. La que me resultó más útil es el histograma.
Una vez que uno entra al histograma presentado por jhat, seguramente va a encontrar algún tipo básico con mayor cantidad de instancias, por ejemplo int o char[], es lógico ya que cualquier objeto está compuesto por tipos básicos, pero inmediatamente abajo se pueden encontrar otras clases con cantidad un poco menor de instancias y probablemente éstas sean las que nos interesan.

Ahora llegamos al punto en el que identificamos cuales son los objetos que están ocupando más memoria, al hacer click en la clase que nos interesa analizar, podemos ver algunos datos interesantes de la misma y debajo una lista de todas las instancias de dicha clase, así que elegimos una instancia cualquiera y podemos empezar a navegar a través de sus referencias, para encontrar el objeto raíz que está dejando las instancias colgadas.

Una vez identificado el objeto, estamos listos para corregir el error, o como en mi caso quejarnos a soporte y tratar de encontrar un workaround!