Introducción: «Romper no es construir»
\nCada desarrollador Java ha maldecido al menos una vez una fuga de memoria (memory leak) en su aplicación en producción. La aplicación falla con OutOfMemoryError, el servidor se reinicia, los usuarios escriben correos enojados. Pero también hay otra cara de la moneda: cómo crear una fuga de memoria en Java intencionalmente. Suena loco, pero precisamente esta pregunta (original de Stack Overflow) ha acumulado miles de visitas.
¿Para qué saber esto? Para entender la mecánica del recolector de basura (GC) y los antipatrones que convierten tu código en un colador. En 2025, cuando Java 21 con hilos virtuales y Loom ya es estándar, las fugas antiguas no han desaparecido, simplemente se disfrazan bajo nuevas características.
\nEn este artículo no solo mostraremos cómo «estropear» una aplicación Java, sino que también daremos las mejores prácticas para detectar fugas en 2025. ¿Listos? ¡Allá vamos!
\n\nUn poco de teoría: por qué Java no es «eterno»
\nMuchos novatos creen firmemente que el recolector de basura (GC) elimina automáticamente todo lo que no se necesita. La realidad es cruel: una fuga de memoria en Java es posible si un objeto permanece alcanzable (reachable) desde las raíces del GC (GC roots), pero la aplicación ya no lo necesita.
\nEn 2025, con la llegada de los Value Objects (Project Valhalla) y el mejorado ZGC (recolector con tiempo de pausa < 1 ms), la situación de rendimiento ha mejorado, pero la lógica de las fugas sigue siendo la misma. El GC no es telépata: no sabe que olvidaste anular una referencia en un HashMap.
Método n.º 1: Clásico — el HashMap «olvidadizo»
\nEl antipatrón más popular que pasa de artículo en artículo. Pero sin él, no se puede.
\nCómo funciona la fuga
\nColocas objetos en un HashMap (o HashSet) como claves, pero olvidas eliminarlos. Si las claves son objetos mutables que cambian su código hash después de la inserción, entonces garantizas una fuga de memoria Java.
import java.util.HashMap;\nimport java.util.Map;\n\npublic class LeakyMapExample {\n public static void main(String[] args) {\n Map<StringBuilder, String> map = new HashMap<>();\n\n for (int i = 0; i < 100_000; i++) {\n StringBuilder key = new StringBuilder("key_" + i);\n map.put(key, "value_" + i);\n // "Olvidamos" eliminar — las claves cuelgan para siempre\n }\n System.out.println("Tamaño del mapa: " + map.size());\n // Fuga: 100_000 objetos StringBuilder + String en el heap\n }\n}\n\n\n¿Por qué es una fuga? Mientras la referencia a map esté viva, todas las claves y valores permanecen en el heap. Incluso si la lógica de la aplicación ya no usa estos datos, el GC no los tocará.
Mejor práctica 2025
\n- \n
- Usa
WeakHashMapsi las claves son objetos temporales. \n - Para almacenamiento en caché, usa
CaffeineoGuava Cachecon límites de tamaño/tiempo. \n - Nunca uses
StringBuildercomo clave — solo tipos inmutables. \n
Método n.º 2: Colecciones estáticas — el asesino silencioso
\nLos campos estáticos viven hasta que la JVM se detiene. Si almacenas datos en un static List o static Map y no los limpias, obtienes una fuga de memoria Java a nivel de clase.
import java.util.ArrayList;\nimport java.util.List;\n\npublic class StaticLeak {\n private static final List<byte[]> CACHE = new ArrayList<>();\n\n public static void addData() {\n // Cada llamada añade 10 MB a la lista estática\n CACHE.add(new byte[10 * 1024 * 1024]);\n }\n\n public static void main(String[] args) throws Exception {\n for (int i = 0; i < 1_000; i++) {\n addData();\n Thread.sleep(100);\n }\n // OutOfMemoryError es inevitable\n }\n}\n\n\n<