1. Dónde se almacenan las variables locales
Empecemos por lo más sencillo: las variables locales. Son variables que se declaran dentro de un método y existen solo durante su ejecución. Su vida es corta y dramática: en cuanto el método termina, todas sus variables locales desaparecen sin dejar rastro.
En Java, las variables locales se almacenan en la pila (stack). Cada hilo tiene su propia pila. Si un método invoca a otro, se añade a la pila un nuevo «frame» (stack frame) con las variables locales y la dirección de retorno. Cuando el método termina, su frame se retira de la pila.
Ejemplo: vida de una variable local
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // la variable local a vive solo en main
printSquare(a);
// ¡Aquí la variable b ya no existe!
}
public static void printSquare(int b) {
int square = b * b; // variable local square
System.out.println("Cuadrado: " + square);
// Tras salir de printSquare todas las variables locales desaparecen
}
}
Punto importante: si una variable local es una referencia a un objeto (por ejemplo, String, Scanner, un array), entonces la referencia vive en la pila, pero el objeto está en el heap. Cuando la referencia desaparece y no hay otras referencias al objeto, el recolector de basura puede eliminarlo.
Ilustración
Stack (para main):
| int a = 42 |
| args |
-------------------
Heap:
| [objetos creados con new] |
2. Fugas de memoria en Java: ¿mito o realidad?
Muchos principiantes piensan: «¡En Java hay recolector de basura! ¡Entonces no puede haber fugas de memoria!». Por desgracia, es un mito que se desvanece en el primer proyecto grande.
El recolector de basura elimina solo aquellos objetos a los que no apunta ninguna referencia viva. Si en algún lugar quedó una referencia (aunque sea en el lugar más inesperado), el objeto permanecerá en memoria hasta el final. O hasta un OutOfMemoryError.
Ejemplo 1: Colección estática trampa
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// Ay, esta colección estática
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("Cadena número " + i);
}
System.out.println("Se han añadido un millón de cadenas");
// Incluso si main termina, BIG_LIST permanecerá en memoria mientras viva la JVM
}
}
¿Qué ocurre?
- La variable estática BIG_LIST vive tanto como el propio class (normalmente, hasta el final de la vida de la JVM).
- Todas las cadenas añadidas a la lista no pueden ser eliminadas por el recolector de basura: siempre hay una referencia a ellas a través de BIG_LIST.
- Si olvidas limpiar colecciones de este tipo, tendrás una fuga de memoria.
Ejemplo 2: Listeners no desregistrados (listeners)
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... otros métodos ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("¡Evento!");
source.addListener(listener);
// Si se olvida llamar a source.removeListener(listener), ¡el listener permanecerá en memoria para siempre!
}
}
Problema: si el listener ya no es necesario pero no se elimina de la lista, él y todos los objetos a los que hace referencia permanecerán en memoria.
Ejemplo 3: Una caché que nunca se vacía
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// Cada vez creamos un array de 1 KB
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("Se han añadido un millón de elementos a la caché");
// La caché crece, la memoria se acaba, ¡OutOfMemoryError!
}
}
Conclusión: ¡incluso con GC es fácil obtener una fuga de memoria si no controlas el ciclo de vida de los objetos!
3. Referencias débiles (WeakReference) y compañía
A veces necesitamos cachés o colecciones donde los objetos puedan ser eliminados por el recolector de basura si ya nadie los referencia. Para ello se inventaron las referencias débiles (WeakReference).
Referencias normales (strong)
String s = new String("hello"); // referencia strong
El objeto s vivirá en memoria mientras exista al menos una referencia strong.
Referencia débil (WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("¡Hola, mundo!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("Antes de la limpieza: " + weak.get()); // hay referencia
strong = null; // eliminamos la referencia strong
System.gc(); // pedimos al GC que limpie memoria (¡no garantizado!)
// Al cabo de un tiempo, weak.get() puede volverse null
System.out.println("Después del GC: " + weak.get());
}
}
¿Cómo funciona?
- Mientras exista al menos una referencia strong al objeto, el GC no lo eliminará.
- Si solo quedan referencias débiles, el objeto puede eliminarse en la siguiente pasada del recolector.
- El método weak.get() devuelve el objeto si todavía está vivo, o null si ya ha sido eliminado.
¿Dónde se usan las referencias débiles?
El uso principal son las cachés, donde no es crítico si el objeto se elimina de memoria. Por ejemplo, si cacheas imágenes pero no quieres que la caché ocupe toda la memoria.
Ejemplo: WeakHashMap
WeakHashMap es una colección donde las claves se almacenan mediante referencias débiles. Si ya nadie referencia la clave, el par correspondiente se elimina del mapa.
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "Valor");
System.out.println("Antes de la limpieza: " + map);
key = null; // eliminamos la referencia strong a la clave
System.gc(); // pedimos al GC que trabaje
// ¡Al cabo de un tiempo el mapa se quedará vacío!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Después del GC: " + map);
}
}
Atención: WeakHashMap funciona solo para claves: los valores se almacenan con referencias strong normales.
Soft, Weak, Phantom: toda la familia de referencias
En Java hay cuatro tipos de referencias (ordenadas por «fuerza»):
| Tipo de referencia | ¿Cuándo elimina el GC el objeto? | ¿Dónde se aplica? |
|---|---|---|
|
Solo cuando no queda ninguna referencia strong | Variables normales, colecciones |
|
Cuando falta memoria | Caché que conviene mantener más tiempo |
|
En el siguiente ciclo del GC, si no hay referencias strong | Cachés, WeakHashMap, listeners |
|
Tras la finalización, para seguir la eliminación del objeto | Tareas especiales, limpieza fuera del heap |
- SoftReference — el objeto se elimina cuando falta memoria (apto para cachés de imágenes y otros).
- WeakReference — se elimina en la primera recolección si no hay otras referencias.
- PhantomReference — el tipo más «fantasmal», necesario para escenarios complejos; los principiantes rara vez lo usan.
4. Práctica: ejemplo de fuga de memoria y su corrección
Ejemplo de fuga: lista estática
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 MB
if (i % 10 == 0) System.out.println("Añadidos " + i + " MB");
}
}
}
¿Qué ocurrirá?
El programa consumirá rápidamente toda la memoria disponible y fallará con un OutOfMemoryError, porque la lista estática guarda referencias a todos los arrays creados.
Corrección: usar referencias débiles
Si no es crítico que todos los objetos estén siempre disponibles, puedes almacenarlos mediante referencias débiles:
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("Añadidos " + i + " MB");
System.gc(); // Sugerencia al GC (¡no garantiza limpieza inmediata!)
}
}
}
Ahora los arrays pueden ser eliminados por el GC si ya nadie los referencia: la lista solo almacena referencias débiles.
5. Escenarios típicos de fugas de memoria
- Listeners de eventos: si olvidas eliminar el listener, el objeto vive para siempre.
- Colecciones estáticas: cachés que no se limpian, listas globales: todo ello puede provocar fugas.
- Clases internas y lambdas: si una clase interna o una lambda captura una referencia al objeto externo, no se eliminará mientras viva el objeto externo.
Ejemplo con clase interna
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB
public Runnable createTask() {
// ¡La clase interna anónima captura una referencia a Outer!
return new Runnable() {
@Override
public void run() {
System.out.println("La tarea se está ejecutando");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// ¡Incluso si outer = null, task seguirá manteniendo una referencia a bigArray!
}
}
Solución: utiliza clases internas estáticas o extrae la lógica a clases independientes para no mantener referencias innecesarias.
6. Práctica: uso de referencias débiles en una caché
Añadamos a la aplicación de ejemplo una caché sencilla usando WeakHashMap.
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 MB
System.out.println("Gatito añadido a la caché");
// Si ya no hay referencias a la clave "cat", el objeto puede ser eliminado por el GC
}
}
En aplicaciones reales (por ejemplo, en bibliotecas de imágenes) las referencias débiles ayudan a evitar el desbordamiento de memoria eliminando automáticamente los datos poco usados.
7. Strong vs Weak: ¿cuándo usar qué?
- Referencias strong — por defecto: utilízalas para todo lo que deba vivir con garantía.
- Referencias débiles — para cachés, listeners, cuando no es crítico si el objeto es eliminado.
- Referencias soft — para cachés que conviene mantener el mayor tiempo posible, pero que pueden eliminarse cuando falte memoria.
- Referencias phantom — para escenarios avanzados (por ejemplo, finalización fuera del heap).
8. Errores típicos al trabajar con memoria y referencias
Error n.º 1: «¡El GC lo limpiará todo por mí!» El recolector de basura elimina solo los objetos a los que no apunta ninguna referencia strong viva. Si «olvidas» una referencia en algún sitio (por ejemplo, en una colección static), el objeto vivirá eternamente.
Error n.º 2: listeners olvidados. ¿Añadiste un listener a un objeto y no lo eliminaste al destruirlo? El listener y todo lo que haya capturado permanecerán en memoria.
Error n.º 3: caché sin referencias débiles. ¿Usas un HashMap normal para una caché que debería limpiarse automáticamente? Mejor usa WeakHashMap o SoftReference.
Error n.º 4: las clases internas y las lambdas capturan el objeto externo. Las clases internas (y las expresiones lambda) almacenan implícitamente una referencia al objeto externo. Si guardas instancias de tales clases más tiempo que el propio objeto externo, tendrás una fuga.
Error n.º 5: esperar que el GC actúe de inmediato. La llamada a System.gc() no garantiza que la recolección se produzca de inmediato. Es solo una «petición» a la JVM, no una orden.
GO TO FULL VERSION