¿Cómo capturar un heap dump en Java?
1. Introducción
En este tutorial, exploraremos diferentes maneras de capturar un heap dump en Java. Un heap dump es una instantánea de todos los objetos que están en memoria en la JVM en un momento determinado. Son muy útiles para solucionar problemas de fugas de memoria y optimizar el uso de memoria en las aplicaciones Java. Los heap dumps generalmente se almacenan en formato binario en archivos con extensión .hprof
. Podemos abrir y analizar estos archivos utilizando herramientas como jhat
o JVisualVM
. También, para los usuarios de Eclipse, es muy común utilizar el Eclipse Memory Analyzer Tool (MAT).
En las siguientes secciones, revisaremos múltiples herramientas y enfoques para generar un heap dump, mostrando las principales diferencias entre ellas.
2. Herramientas del JDK
El JDK viene con varias herramientas para capturar heap dumps de diferentes maneras. Todas estas herramientas se encuentran bajo la carpeta bin
dentro del directorio de inicio de JDK, por lo que podemos iniciarlas desde la línea de comandos siempre que este directorio esté incluido en la ruta del sistema.
A continuación, veremos cómo utilizar estas herramientas para capturar heap dumps.
2.1. jmap
jmap
es una herramienta para imprimir estadísticas sobre la memoria en una JVM en ejecución. La podemos usar para procesos locales o remotos.
Para capturar un heap dump utilizando jmap
, necesitamos usar la opción dump
:
jmap -dump:[live],format=b,file=<file-path> <pid>
Debemos especificar varios parámetros:
- live: si se establece, solo imprime objetos que tienen referencias activas y descarta los que están listos para ser recolectados por el garbage collector. Este parámetro es opcional.
- format=b: especifica que el archivo dump será en formato binario. Si no se establece, el resultado será el mismo.
- file: el archivo donde se escribirá el dump.
- pid: id del proceso Java.
Un ejemplo podría verse así:
jmap -dump:live,format=b,file=/tmp/dump.hprof 12587
Recuerda que podemos obtener fácilmente el pid de un proceso Java utilizando el comando jps
.
Es importante mencionar que jmap
fue introducido en el JDK como una herramienta experimental y no es soportada. Por lo tanto, en algunos casos, puede ser preferible utilizar otras herramientas en su lugar.
2.2. jcmd
jcmd
es una herramienta muy completa que funciona enviando solicitudes de comando a la JVM. Debemos usarla en la misma máquina donde se está ejecutando el proceso Java.
Uno de sus muchos comandos es GC.heap_dump
. Podemos usarlo para obtener un heap dump simplemente especificando el pid del proceso y la ruta del archivo de salida:
jcmd <pid> GC.heap_dump <file-path>
Podemos ejecutarlo con los mismos parámetros que utilizamos anteriormente:
jcmd 12587 GC.heap_dump /tmp/dump.hprof
Al igual que con jmap
, el dump generado es en formato binario.
2.3. JVisualVM
JVisualVM
es una herramienta con una interfaz gráfica de usuario que nos permite monitorear, solucionar problemas y perfilar aplicaciones Java. La interfaz es simple, pero muy intuitiva y fácil de usar.
Una de sus muchas opciones nos permite capturar un heap dump. Si hacemos clic derecho en un proceso Java y seleccionamos la opción “Heap Dump”, la herramienta creará un heap dump y lo abrirá en una nueva pestaña:
Observe que podemos encontrar la ruta del archivo creado en la sección “Basic Info”.
A partir de JDK 9, Visual VM no está incluido en las distribuciones de Oracle JDK y Open JDK. Por lo tanto, si estamos utilizando cualquier versión de Java posterior a 9, podemos obtener el JVisualVM
desde el sitio del proyecto Visual VM.
3. Capturar un Heap Dump Automáticamente
Todas las herramientas que hemos presentado anteriormente están destinadas a capturar heap dumps manualmente en un momento específico. En algunos casos, queremos obtener un heap dump cuando ocurre un java.lang.OutOfMemoryError
para ayudarnos a investigar el error.
Para estos casos, Java proporciona la opción de línea de comandos HeapDumpOnOutOfMemoryError
, que genera un heap dump cuando se lanza un java.lang.OutOfMemoryError
:
java -XX:+HeapDumpOnOutOfMemoryError
Por defecto, almacena el dump en un archivo java_pid<pid>.hprof
en el directorio donde estamos ejecutando la aplicación. Si queremos especificar otro archivo o directorio, podemos configurarlo en la opción HeapDumpPath
:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<file-or-dir-path>
Cuando nuestra aplicación se queda sin memoria utilizando esta opción, podremos encontrar el archivo creado que contiene el heap dump en los logs:
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
Dumping heap to java_pid12587.hprof ...
Exception in thread "main" Heap dump file created [4744371 bytes in 0.029 secs]
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at com.baeldung.heapdump.App.main(App.java:7)
En el ejemplo anterior, se escribió en el archivo java_pid12587.hprof
.
Como podemos ver, esta opción es muy útil, y no hay sobrecarga al ejecutar una aplicación con esta opción. Por lo tanto, se recomienda encarecidamente usar esta opción, especialmente en producción.
Por último, esta opción también puede especificarse en tiempo de ejecución utilizando el MBean HotSpotDiagnostic
. Para hacer esto, podemos usar JConsole
y establecer la opción VM HeapDumpOnOutOfMemoryError
a true
:
Podemos encontrar más información sobre MBeans y JMX en el artículo sobre Java Management Extensions.
4. JMX
El último enfoque que cubriremos en este artículo es usar JMX. Utilizaremos el HotSpotDiagnostic
MBean que introdujimos brevemente en la sección anterior. Este MBean proporciona un método dumpHeap
que acepta dos parámetros:
- outputFile: la ruta del archivo para el dump. Este archivo debe tener la extensión
.hprof
. - live: si se establece en verdadero, solo se vuelcan los objetos activos en memoria, como vimos anteriormente con
jmap
.
A continuación, mostraremos dos formas diferentes de invocar este método para capturar un heap dump.
4.1. JConsole
La manera más fácil de usar el HotSpotDiagnostic
MBean es utilizando un cliente JMX, como JConsole
.
Si abrimos JConsole
y nos conectamos a un proceso Java en ejecución, podemos navegar a la pestaña MBeans y encontrar el HotSpotDiagnostic
bajo com.sun.management
. En operaciones, encontraremos el método dumpHeap
que describimos anteriormente.
Como se muestra, solo necesitamos introducir los parámetros outputFile y live en los campos de texto p0
y p1
para realizar la operación dumpHeap
.
4.2. Forma Programática
La otra manera de usar el HotSpotDiagnostic
MBean es invocándolo programáticamente desde el código Java.
Para hacer esto, primero necesitamos obtener una instancia de MBeanServer
para obtener un MBean que está registrado en la aplicación. Después, simplemente necesitamos obtener una instancia de HotSpotDiagnosticMXBean
, y llamar a su método dumpHeap
.
Veamos un ejemplo en código:
public static void dumpHeap(String filePath, boolean live) throws IOException {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
HotSpotDiagnosticMXBean mxBean = ManagementFactory.newPlatformMXBeanProxy(
server, "com.sun.management:type=HotSpotDiagnostic", HotSpotDiagnosticMXBean.class);
mxBean.dumpHeap(filePath, live);
}
Es importante notar que un archivo .hprof
no se puede sobrescribir. Por lo tanto, debemos tener esto en cuenta al crear una aplicación que imprima heap dumps. Si no lo hacemos, obtendremos una excepción:
Exception in thread "main" java.io.IOException: File exists
at sun.management.HotSpotDiagnostic.dumpHeap0(Native Method)
at sun.management.HotSpotDiagnostic.dumpHeap(HotSpotDiagnostic.java:60)
5. Conclusión
En este artículo, aprendimos múltiples formas de capturar un heap dump en Java.
Como regla general, siempre debemos recordar usar la opción HeapDumpOnOutOfMemoryError
al ejecutar aplicaciones Java. Para diferentes propósitos, cualquiera de las otras herramientas se puede usar, siempre que tengamos en cuenta el estado no soportado de jmap
.
Recuerda implementar estas técnicas en tus aplicaciones para mejorar el manejo de memoria y la estabilidad en producción. Además, practicar el análisis de los heap dumps generados te permitirá adquirir habilidades valiosas a medida que desarrollas y mantienes aplicaciones Java más complejas.