CodeGym /Cursos /JAVA 25 SELF /Análisis de errores típicos al trabajar con memoria

Análisis de errores típicos al trabajar con memoria

JAVA 25 SELF
Nivel 64 , Lección 4
Disponible

1. Errores típicos al trabajar con memoria

Es hora de mirar el reverso de la magia de la gestión automática de memoria. Incluso si no programas en C, donde hay que vigilar cada byte, en Java puedes liarla tanto que la aplicación devore memoria como un gato glotón — salchicha. Veamos los errores más comunes y cómo evitarlos.

Listeners olvidados (escuchadores)

En Java se usa a menudo el patrón «listener»: un objeto que se suscribe a los eventos de otro objeto. Por ejemplo, creas un botón y le añades un manejador de clic:

button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // procesamiento del clic
    }
});

Problema: si olvidaste eliminar ese listener (removeActionListener) cuando el botón o la ventana ya no son necesarios, el listener se queda colgado en memoria. Aunque cierres la ventana y anules todas sus referencias, el objeto listener sigue manteniendo una referencia a la ventana (o al revés), impidiendo que el recolector de basura libere la memoria.

Analogía: Imagina que te mudaste pero olvidaste darte de baja de la newsletter de la pizzería: te siguen enviando publicidad a la dirección antigua.

Colecciones estáticas que no se limpian

Los campos estáticos viven tanto como la clase (y a veces — hasta el final de la vida de la aplicación). Si tienes una colección estática:

public class Cache {
    public static final List<String> globalList = new ArrayList<>();
}

y vas añadiendo objetos ahí sin eliminarlos, se quedarán en memoria para siempre. Incluso si no hay más referencias a esos objetos en ningún otro sitio, la referencia desde la colección estática impedirá que el GC los retire.

Ejemplo real: Caché de fotos en una aplicación de escritorio que nunca se limpia. Tras un par de horas de uso — OutOfMemoryError.

No liberar recursos (archivos, flujos, conexiones)

Aunque Java libera memoria, no cierra automáticamente descriptores de archivos, conexiones de red y otros recursos externos. Si olvidas cerrar un archivo o un flujo, el recurso se quedará abierto y, en algún momento, el sistema dirá: «¡Se acabó, no te doy más archivos!» (IOException: Too many open files).

Consejo: Usa siempre try-with-resources:

try (FileInputStream in = new FileInputStream("data.txt")) {
    // leemos el archivo
} // in.close() se invocará automáticamente!

Objetos grandes que permanecen mucho tiempo en memoria

A veces creas un array o una colección grande, lo usas y luego «olvidas soltarlo». Por ejemplo:

List<byte[]> bigList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    bigList.add(new byte[1024 * 1024]); // 1 MB cada uno
}
// ... olvidamos limpiar bigList

Si esta colección vive en un campo estático o en un objeto que tarda en eliminarse, toda esa memoria quedará ocupada.

Clases internas y anónimas: captura de referencias externas

Las clases anónimas (e internas) en Java almacenan una referencia implícita al objeto externo:

public class Outer {
    void doSomething() {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from inner!");
            }
        };
        // r se guarda en algún sitio
    }
}

Si el objeto r acaba en una colección estática o en un caché, «mantendrá» una referencia a la instancia de Outer, incluso si esta ya no es necesaria. Resultado: fuga de memoria. Con las expresiones lambda la situación es algo mejor, pero si la lambda usa campos de la clase externa, la referencia se mantiene igualmente.

2. Errores al trabajar con el recolector de basura

Invocación forzada de System.gc()

Muchos principiantes piensan: «Se acaba la memoria — ¡llamo a System.gc() y asunto resuelto!». En realidad, no es más que una petición a la JVM, no una garantía de recolección inmediata. Usarlo con frecuencia puede empeorar el rendimiento de forma drástica, provocando pausas largas y tirones. En aplicaciones reales es mejor confiar en la JVM — ella decide cuándo recolectar basura. Por cierto, algunas JVM pueden ignorar las llamadas explícitas al GC (por ejemplo, con la opción -XX:+DisableExplicitGC).

Ignorar los registros del GC

En los registros del GC se ve cuándo se producen las recolecciones, cuánto tiempo duran y cuánta memoria se libera. Si no observas esos registros, puedes pasar por alto señales de problemas: pausas largas, Full GC frecuentes, fugas de memoria.

Cómo activar los registros de GC:

java -Xlog:gc* -jar MyApp.jar

o para JVM antiguas:

java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar MyApp.jar

Elección incorrecta del GC para la tarea

La elección del recolector afecta a las latencias y a la estabilidad. Para baja latencia (bolsas, juegos online) el stop-the-world paralelo Parallel GC es mala idea: puede «congelar» todos los hilos durante la recolección. Considera G1 GC, ZGC o Shenandoah.

3. Errores con las colecciones

Usar HashMap en lugar de WeakHashMap para cachés.
Si haces un caché donde los objetos deben eliminarse automáticamente cuando ya no hay referencias «vivas» a ellos, usa WeakHashMap:

Map<Key, Value> cache = new WeakHashMap<>();

Con un HashMap normal los objetos vivirán hasta que limpies el caché manualmente, lo que provocará una fuga de memoria.

Olvidar remove() para los elementos.
Si añades objetos a colecciones (por ejemplo, a listas de listeners) pero no los eliminas cuando ya no son necesarios, esos objetos vivirán para siempre, especialmente en colecciones de larga vida (por ejemplo, estáticas).

4. Buenas prácticas: cómo evitar problemas

Elimina siempre los listeners.
Si un objeto se ha suscrito a eventos, asegúrate de desuscribirlo cuando ya no sea necesario. Es cómodo hacerlo en el método dispose() o al cerrar la ventana/pantalla.

button.removeActionListener(myListener);

Usa referencias débiles para cachés.
Si en el caché puedes prescindir de la garantía de conservación del objeto, utiliza WeakReference o colecciones basadas en ellas (WeakHashMap). Así el GC podrá liberar memoria cuando se necesite.

Monitoriza la memoria en producción.
Usa jvisualvm, jconsole o sistemas APM. Esto ayuda a detectar fugas antes de que lleguen las quejas de los usuarios.

Analiza el heap dump si sospechas una fuga

Si la aplicación empezó a «devorar» más memoria de lo habitual, toma un heap dump (por ejemplo, con jmap o jvisualvm) y mira qué objetos ocupan más espacio. A menudo el culpable se encuentra en un par de minutos.

Ajusta los parámetros de la JVM.

  • -Xmx — tamaño máximo del heap
  • -Xms — tamaño inicial del heap

Límites razonables ayudan a evitar OutOfMemoryError y aceleran el diagnóstico.

5. Práctica: ejemplo de código con fuga de memoria y su corrección

Ejemplo 1: Fuga a través de una colección estática

public class MemoryLeakDemo {
    // Colección estática: vive para siempre
    private static final List<byte[]> leakyList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            leakyList.add(new byte[1024 * 1024]); // 1 MB cada vez
            System.out.println("Añadido " + (i + 1) + " MB");
        }
        // OutOfMemoryError!
    }
}

Solución: Usa una variable local o limpia la colección cuando ya no sea necesaria.

public class MemoryLeakFixed {
    public static void main(String[] args) {
        List<byte[]> tempList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            tempList.add(new byte[1024 * 1024]);
            System.out.println("Añadido " + (i + 1) + " MB");
        }
        // tempList = null; // Se puede poner a null explícitamente
        // Ahora los objetos están disponibles para el GC tras salir del método
    }
}

Ejemplo 2: Fuga a través de un listener

public class Window {
    private final List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener l) {
        listeners.add(l);
    }
    // No hay método removeListener!
}

Solución: Añade un método para eliminar el listener y llámalo al cerrar la ventana.

public void removeListener(EventListener l) {
    listeners.remove(l);
}

Ejemplo 3: Caché con HashMap en lugar de WeakHashMap

Map<Object, Object> cache = new HashMap<>();
// ... añadimos objetos

Solución: Cambia a WeakHashMap:

Map<Object, Object> cache = new WeakHashMap<>();

Consejos para configurar la JVM para monitorizar la memoria

  • Activa los registros de GC: -Xlog:gc* o -XX:+PrintGCDetails
  • Limita el tamaño máximo del heap: -Xmx512m
  • Si usas cachés — controla su tamaño y utiliza referencias débiles si es aceptable
  • Experimenta con el GC: -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseShenandoahGC

7. Errores típicos al trabajar con memoria

Error n.º 1: listeners y suscripciones olvidados. Si añadiste un listener a un objeto pero olvidaste eliminarlo, el objeto listener (y todo lo que referencia) se quedará en memoria. Un clásico en GUI y sistemas dirigidos por eventos. Usa removeListener/removeActionListener.

Error n.º 2: colecciones estáticas sin limpieza. Los campos estáticos viven más que nadie. Si metes objetos en ellos y no limpias la colección, esos objetos se quedarán en memoria para siempre. Especialmente traicionero con cachés sin límite.

Error n.º 3: no liberar recursos externos. ¿Dejaste un flujo, archivo o conexión abiertos? Pierdes memoria y topas con los límites del SO. Usa try-with-resources y cierra los recursos.

Error n.º 4: invocación forzada de System.gc(). No es una panacea, solo una petición a la JVM. A menudo provoca pausas y degradación del rendimiento.

Error n.º 5: colecciones normales para cachés. Si los objetos del caché deben eliminarse por sí solos, aplica referencias débiles/soft (WeakHashMap, SoftReference). De lo contrario, tendrás una fuga.

Error n.º 6: clases internas y anónimas que capturan referencias externas. Las clases internas y las lambdas pueden retener implícitamente una referencia al objeto externo. Si se guardan en una colección de larga vida — es una fuga.

Error n.º 7: ignorar los registros del GC. Si no miras los registros del GC, no sabrás de pausas largas o Full GC frecuentes — y tus usuarios lo sabrán por los tirones y bloqueos. Activa -Xlog:gc* o -XX:+PrintGCDetails.

1
Cuestionario/control
Memoria y recolección de basura, nivel 64, lección 4
No disponible
Memoria y recolección de basura
Memoria y recolección de basura
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION