HOWTO · Java

Encuentra fugas de memoria en Java

Este artículo explica cómo encontrar pérdidas de memoria en Java.

En esta página

Una fuga de memoria en Java ocurre cuando una aplicación mantiene accesibles objetos que ya no son útiles. El recolector de basura recupera los objetos inaccesibles, pero no puede recuperar uno al que todavía se llega mediante una referencia activa. A medida que se acumulan objetos retenidos, puede aumentar el uso de memoria y, con el tiempo, contribuir a un java.lang.OutOfMemoryError. Un uso elevado de memoria por sí solo no demuestra que haya una fuga; investiga si el conjunto de objetos vivos crece al repetir cargas de trabajo comparables.

¿Qué es una fuga de memoria en Java?

Java administra automáticamente la memoria de los objetos, así que una fuga suele significar que el programa conserva referencias más tiempo del previsto, no que haya dejado de liberar memoria manualmente. Un objeto puede haber quedado obsoleto para la aplicación y, aun así, seguir siendo accesible desde un campo estático, un servicio de larga duración, una colección u otro objeto. El recolector de basura conserva correctamente esos objetos accesibles. Esta distinción ayuda a separar la retención no deseada de un pico temporal de asignación o del crecimiento normal del heap.

Causas comunes de las fugas de memoria en Java

Busca errores de propiedad y de ciclo de vida que permitan que las referencias sobrevivan a su propósito. Algunos ejemplos comunes son las cachés sin límite, los objetos de sesión que se conservan después de que esta termina, las colecciones que reciben entradas continuamente sin eliminar las antiguas, los listeners que siguen registrados cuando su consumidor ya no los necesita y las estructuras de datos personalizadas que retienen nodos obsoletos. Son pistas para investigar, no pruebas de que un objeto concreto tenga una fuga. Comprueba quién es responsable de cada referencia y cuándo debería liberarla.

Flujo práctico para encontrar una fuga de memoria en Java

Empieza por reproducir la misma carga de trabajo en ciclos repetibles. Cuando cada ciclo haya tenido tiempo de completarse y se haya ejecutado el recolector de basura, registra el nivel de memoria de los objetos vivos. Compara puntos equivalentes en lugar de picos de asignación que no guardan relación entre sí. Compara la memoria de objetos vivos después de GC en ciclos repetidos de cargas de trabajo equivalentes; un nivel que sube es una señal para investigar, no una prueba de fuga.

Diagrama de ciclos de carga de trabajo equivalentes, aumento de la memoria de objetos vivos después de GC, comparación de volcados del heap, seguimiento de referencias que retienen objetos, corrección y nueva prueba.

Bucle de investigación para distinguir un conjunto de objetos retenidos que crece de una asignación temporal.

Si el nivel sigue subiendo, captura evidencia del heap en puntos comparables de la carga de trabajo. Compara la cantidad de objetos y su tamaño retenido, sigue los objetos sospechosos hasta las referencias que los mantienen accesibles e identifica al responsable cuyo ciclo de vida es demasiado largo. Haz un cambio específico; después, repite la misma carga de trabajo y compara las mismas mediciones. Una sola muestra puede inducir a error, así que ten en cuenta la variación de la carga y el comportamiento normal de la aplicación antes de concluir que hay una fuga.

Ejecuta este ejemplo autocontenido de Java para ver cómo eliminar entradas libera las referencias que la colección mantiene a ellas. Las cadenas impresas muestran el número de entradas antes y después de vaciar el mapa; sus etiquetas permanecen en inglés:

import java.util.HashMap;
import java.util.Map;

public class CacheLifecycle {
    public static void main(String[] args) {
        Map<Integer, byte[]> cache = new HashMap<>();
        for (int i = 0; i < 3; i++) {
            cache.put(i, new byte[1024]);
        }
        System.out.println("Entries before cleanup: " + cache.size());
        cache.clear();
        System.out.println("Entries after cleanup: " + cache.size());
    }
}

Este pequeño ejemplo muestra la propiedad de la colección y la limpieza; no mide el uso del heap ni demuestra que exista una fuga en producción. En una aplicación real, elimina una entrada solo cuando su ciclo de vida o la política de caché indiquen que ya no hace falta.

Analizar volcados del heap y objetos retenidos

Un volcado del heap registra los objetos y sus referencias en un momento concreto. Úsalo para encontrar tipos de objetos con una cantidad inusualmente alta de instancias, examinar su tamaño retenido y seguir las rutas de retención hasta raíces de larga duración, como campos estáticos o servicios de la aplicación. Compara volcados tomados después de ciclos equivalentes de carga de trabajo para ver qué grupos de objetos crecen y siguen siendo accesibles. Un volcado identifica un estado que conviene investigar; por sí solo no demuestra que haya una fuga. Confirma la sospecha de retención comparándola con el ciclo de vida esperado de la aplicación.

Usar perfiladores y herramientas de supervisión de JVM

La supervisión puede mostrar si el uso del heap, las asignaciones o el comportamiento de la recolección cambian con el tiempo. Un perfilador puede ayudar a relacionar los grupos de objetos y la actividad de asignación con el comportamiento de la aplicación; después, el análisis del volcado del heap puede revelar por qué las instancias siguen siendo accesibles. NetBeans Profiler, VisualVM y JProfiler son ejemplos presentes en flujos de trabajo de perfilado de Java, no recomendaciones sobre disponibilidad o funciones actuales. Elige una herramienta adecuada para la JVM y el entorno que investigas, e interpreta sus gráficos y advertencias como evidencia, no como un diagnóstico por sí mismos.

Inspeccionar los registros de recolección de basura

La opción de JVM -verbose:gc es un ejemplo del material de origen para habilitar registros detallados de recolección de basura. Su aplicabilidad, así como el formato y la interpretación del registro resultante, dependen de la versión de JVM. Revisa el comportamiento de la recolección y la memoria que queda después de ella en intervalos comparables de carga de trabajo. Un registro puede ayudar a mostrar que queda memoria viva tras la recolección, pero un resumen por sí solo no identifica qué referencias retienen los objetos ni establece la causa.

Entender las advertencias de fugas de memoria de Eclipse y sus límites

Las advertencias de Eclipse pueden llamar la atención sobre ciertos patrones de recursos o referencias en el código. Lee cada advertencia en contexto y verifica el ciclo de vida del objeto y del recurso correspondiente. Una advertencia del IDE es una señal parcial: no demuestra que cada advertencia corresponda a una fuga de memoria, y la ausencia de advertencias tampoco demuestra que la aplicación esté libre de fugas. No uses la antigua afirmación sobre compatibilidad con JDK 1.5 como descripción general del comportamiento actual de detección.

Verificar la corrección y reducir la memoria retenida

Después de cambiar al responsable sospechoso o la ruta de limpieza, repite la misma carga de trabajo y compara, en puntos equivalentes, la memoria de objetos vivos después de GC, la cantidad de objetos y las rutas de retención. Comprueba que los objetos necesarios sigan disponibles y que los obsoletos ya no se retengan. Si el nivel sigue creciendo, examina la nueva evidencia y continúa siguiendo las referencias. Informa de mejoras de rendimiento solo cuando las respalden mediciones en condiciones comparables; el objetivo es verificar el cambio en la retención, no dar por hecho que cualquier reducción de memoria acelera la aplicación.