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.
GO TO FULL VERSION