La JVM es una caja negra extraordinariamente eficiente. Administra memoria, hilos, garbage collection y compilación JIT sin que tengas que pensar en ello — hasta que algo falla. Y cuando falla en producción, la JVM no te dice qué pasó. Tienes que preguntarle. Las herramientas para preguntar son dos: thread dumps y heap dumps.
He diagnosticado deadlocks a las 2 de la madrugada, memory leaks que tardaron semanas en manifestarse, y pools de threads saturados que derribaron plataformas enteras. En todos los casos, la respuesta estaba en un dump. El problema es que muchos ingenieros nunca han leído uno, y cuando lo necesitan, no saben por dónde empezar.
Thread Dumps: qué está haciendo cada hilo
Un thread dump es una captura instantánea del estado de todos los threads de la JVM en un momento dado. Cada thread aparece con su nombre, su estado, y su stack trace completo. Es la herramienta fundamental para diagnosticar problemas de concurrencia, contención de recursos y bloqueos.
Los estados que vas a encontrar son cuatro, y cada uno cuenta una historia diferente:
- RUNNABLE: el thread está ejecutando código activamente o está listo para ejecutar. Si ves muchos threads RUNNABLE haciendo la misma operación, tienes un hotspot de CPU.
- WAITING: el thread espera indefinidamente a que otro thread lo notifique. Típico de pools de conexiones vacíos o colas sin productores.
- BLOCKED: el thread intenta adquirir un lock que otro thread tiene. Si ves muchos threads BLOCKED en el mismo monitor, tienes contención seria.
- TIMED_WAITING: como WAITING, pero con timeout. Thread.sleep(), Object.wait(timeout), o esperando respuesta de I/O con límite de tiempo.
Cómo capturar un thread dump
Hay tres métodos principales, y los tres son seguros en producción. No detienen la JVM, no causan pausa significativa, y no requieren reinicio:
- jstack <PID>: el método clásico. Viene con el JDK. Rápido y directo. Si la JVM no responde, usa
jstack -Fpara forzar. - kill -3 <PID>: envía la señal SIGQUIT al proceso Java. El dump se escribe en stdout (o el log del contenedor). Funciona incluso cuando jstack no puede conectarse.
- jcmd <PID> Thread.print: la opción moderna. Más flexible que jstack y con mejor formato de salida. Mi preferida para JVMs recientes.
Un solo thread dump muestra un instante. Para diagnosticar problemas intermitentes, captura tres o cuatro con intervalos de 5-10 segundos. Los threads que aparecen en el mismo estado y la misma línea de código en todas las capturas son los sospechosos.
Patrones de diagnóstico en thread dumps
Después de leer cientos de thread dumps, los patrones se repiten. Estos son los más frecuentes:
- Deadlock: dos o más threads bloqueados esperando un lock que tiene el otro. La JVM los detecta automáticamente y los reporta al final del dump con el mensaje "Found one Java-level deadlock". Si ves esto, revisa el orden de adquisición de locks en tu código.
- Thread starvation: el pool de threads está agotado. Todos los worker threads están ocupados (RUNNABLE o BLOCKED) y las nuevas peticiones se acumulan en la cola. Aparece cuando el pool es demasiado pequeño o cuando hay operaciones bloqueantes que deberían ser asíncronas.
- Pool saturation: variante del anterior específica de connection pools. Muchos threads en WAITING en
getConnection()oborrowObject(). La base de datos o el servicio downstream no puede atender la demanda. - Lock contention: decenas de threads en BLOCKED esperando el mismo monitor. Un solo lock se convierte en cuello de botella. Solución: reducir la sección crítica, usar locks más granulares, o considerar estructuras lock-free.
Para análisis automatizado, herramientas como IBM Thread Analyzer y fastthread.io parsean el dump, agrupan threads por estado, detectan deadlocks y visualizan la contención. fastthread.io es particularmente útil porque funciona desde el navegador: subes el archivo y obtienes un informe inmediato.
Heap Dumps: qué hay en memoria
Si el thread dump muestra qué está haciendo la JVM, el heap dump muestra qué está almacenando. Es una captura completa del heap: cada objeto, su tipo, su tamaño, y sus referencias a otros objetos. Es la herramienta definitiva para diagnosticar memory leaks y presión de garbage collection.
Un heap dump puede pesar varios gigabytes — es proporcional al tamaño del heap configurado. Capturarlo causa una pausa en la JVM (stop-the-world) cuya duración depende del tamaño del heap. En producción, hay que ser consciente de este impacto.
Cómo capturar un heap dump
- jmap -dump:format=b,file=heap.hprof <PID>: captura bajo demanda. Úsalo cuando necesitas diagnosticar un problema activo.
- -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/: la más importante. Configúrala siempre en producción. Cuando la JVM se queda sin memoria, genera automáticamente el dump antes de morir. Sin esto, pierdes la evidencia del crimen.
- jcmd <PID> GC.heap_dump /path/heap.hprof: alternativa moderna a jmap. Misma funcionalidad, mejor integración con herramientas de gestión.
Análisis con Eclipse MAT
Eclipse Memory Analyzer Tool (MAT) es la herramienta estándar para analizar heap dumps. Es gratuita, robusta, y puede manejar dumps de decenas de gigabytes. Los tres análisis fundamentales son:
- Dominator Tree: muestra los objetos que retienen más memoria. Si un único objeto domina el 60% del heap, ahí está tu problema. El dominator tree te dice exactamente qué objeto impide que el garbage collector libere memoria.
- Leak Suspects: MAT ejecuta heurísticas para identificar posibles leaks automáticamente. No siempre acierta, pero es un excelente punto de partida. El reporte incluye la cadena de referencias desde el GC root hasta el objeto sospechoso.
- Histograms: lista de todas las clases con el número de instancias y el tamaño total. Compara histogramas de dos dumps tomados en momentos diferentes: las clases cuyo conteo crece son las candidatas a leak.
Patrones de memory leak
Los memory leaks en Java no son como en C — no es memoria no liberada. Son objetos que el garbage collector no puede liberar porque todavía tienen una referencia activa, aunque la aplicación ya no los necesite. Los patrones más comunes:
- Cache sin límite: un HashMap que crece indefinidamente porque nunca se eliminan entradas. Solución: usar caches con eviction policy (Caffeine, Guava Cache) o WeakHashMap.
- Conexiones no cerradas: connections a base de datos, streams de archivos, o clientes HTTP que se abren y nunca se cierran. Se acumulan en el heap y eventualmente agotan también los file descriptors del sistema operativo.
- Class loader leaks: típico en application servers. Se redespliega la aplicación pero el class loader anterior no se libera porque algún thread o referencia estática lo retiene. El heap crece con cada redespliegue hasta que la JVM explota.
- Listeners no desregistrados: registras un observer o event listener pero nunca lo eliminas. El publisher mantiene la referencia y el objeto nunca se recolecta.
Caso real: memory leak en Sterling OMS
Un entorno de producción de IBM Sterling ejecutaba business processes que procesaban órdenes de compra. El heap crecía de forma constante: 4 GB tras arrancar, 6 GB después de 48 horas, OutOfMemoryError después de una semana. El equipo reiniciaba la JVM cada 5 días como "solución".
Configuramos -XX:+HeapDumpOnOutOfMemoryError y esperamos. Cuando el OOM llegó, analizamos
el dump con Eclipse MAT. El Dominator Tree reveló que un solo objeto — una instancia de
java.util.ArrayList — retenía 2.3 GB de heap. Esa lista vivía dentro de un cache
de documentos procesados que nunca ejecutaba eviction.
El Leak Suspects de MAT señaló directamente la cadena: GC Root → ThreadLocal → BusinessProcessContext → DocumentCache → ArrayList con 1.2 millones de entradas. Cada documento procesado se añadía al cache para "reutilización" pero jamás se eliminaba.
La solución fue configurar un límite de entradas en el cache y un TTL de 30 minutos. El heap se estabilizó en 3.5 GB y no volvió a crecer. Sin el heap dump, el equipo habría seguido reiniciando la JVM indefinidamente.
No adivines qué pasa en la JVM. Mídelo. Un thread dump toma 2 segundos de capturar y puede ahorrarte horas de especulación. Un heap dump es la diferencia entre "creo que hay un leak" y "sé exactamente dónde está el leak".