Recolección de basura en Java
Cómo funciona el recolector de basura de Java: alcanzabilidad, raíces GC, heap generacional, mark-sweep-compact, tipos de referencia y recolectores.
En Java nunca se llama a free(). La JVM rastrea cada objeto que se asigna en el heap y, cuando un objeto ya no puede ser alcanzado por el programa en ejecución, el recolector de basura (GC) reclama su memoria automáticamente. Tú escribes código que crea objetos; el GC limpia silenciosamente detrás de ti. Comprender cómo decide qué es basura — y dónde lo busca en el heap — marca la diferencia entre código que escala y código que se atasca bajo carga.
Esta página cubre cómo el GC decide qué conservar (alcanzabilidad), cómo está organizado el heap para una recolección eficiente, el algoritmo mark-sweep-compact, los cuatro niveles de fuerza de referencia, cómo elegir un recolector, y una demostración ejecutable que hace observable la recolección.
Alcanzabilidad y raíces GC
El GC no busca objetos con los que ya hayas "terminado". Busca objetos que aún son alcanzables. Partiendo de un conjunto de raíces GC, sigue cada referencia. Todo lo que puede alcanzar está vivo; el resto es basura, independientemente de si crees que aún lo necesitas.
| Raíz GC | Ejemplo |
|---|---|
| Variables locales | Una referencia en la pila de un hilo en ejecución |
| Campos estáticos | static final Logger LOG = ... |
| Hilos activos | Un objeto Thread vivo |
| Referencias JNI | Objetos mantenidos por código nativo |
Asignar null a una referencia (o dejar que salga del ámbito) no elimina nada — simplemente elimina un camino hacia el objeto. El objeto solo pasa a ser recolectable cuando no queda ningún camino desde ninguna raíz.
Object a = new Object(); // reachable via local variable 'a'
Object b = a; // now two references point to the same object
a = null; // still reachable through 'b' — not garbage
b = null; // now unreachable — eligible for collectionEl heap generacional
La mayoría de los objetos mueren jóvenes — el ámbito de una solicitud, un temporal de bucle, una cadena intermedia. La JVM explota esta hipótesis generacional débil dividiendo el heap en regiones y recolectando la zona joven con mucha más frecuencia que la antigua.
| Región | Contiene | Recolección |
|---|---|---|
| Young (Eden + 2 espacios Survivor) | Objetos recién asignados | Con frecuencia, mediante un rápido minor GC |
| Old (Tenured) | Objetos que sobrevivieron varios minor GC | Raramente, mediante un major/full GC más lento |
| Metaspace | Metadatos de clases (no tus objetos) | Cuando se descargan los classloaders |
Los nuevos objetos llegan a Eden. Un minor GC copia los pocos supervivientes a un espacio Survivor; los objetos que siguen sobreviviendo son finalmente promovidos a la generación antigua. Como los minor GC solo escanean la pequeña región joven, son baratos — por eso la asignación de objetos de corta vida en Java es rápida. (Para conocer las diferencias entre la pila y el heap, consulta Stack vs Heap; para el rol de la JVM que aloja el heap, consulta Arquitectura de la JVM.)
Marcar, barrer, compactar
Una recolección se ejecuta en fases. Primero marca cada objeto alcanzable recorriendo el grafo desde las raíces. Luego barre, liberando los objetos no marcados. Muchos recolectores añaden una fase de compactación que agrupa los objetos supervivientes para que el espacio libre sea un bloque contiguo — lo que mantiene la asignación como un simple incremento de puntero y evita la fragmentación.
// Pseudocode of what the collector does for you:
// 1. mark: visit(roots); for each reachable object, set live = true
// 2. sweep: for each object on the heap, if !live -> reclaim its memory
// 3. compact: move survivors next to each other, update referencesPuedes sugerir una recolección con System.gc(), pero es solo una sugerencia — la JVM puede ignorarla. Nunca dependas de ello para la corrección; trátalo como una herramienta de diagnóstico, no como una estrategia de gestión de memoria.
Fuerza de referencia
No todas las referencias mantienen un objeto vivo de la misma manera. El paquete java.lang.ref te permite indicarle al GC con qué urgencia quieres conservar un objeto, lo que es la base de las cachés sensibles a la memoria.
| Referencia | Comportamiento del GC |
|---|---|
Fuerte (el = ordinario) | Nunca recolectada mientras sea alcanzable |
SoftReference | Recolectada solo cuando la memoria es escasa — ideal para cachés |
WeakReference | Recolectada en el próximo GC cuando no quedan referencias fuertes |
PhantomReference | Se usa para programar limpieza tras la recolección |
import java.lang.ref.WeakReference;
byte[] data = new byte[1024];
WeakReference<byte[]> ref = new WeakReference<>(data);
data = null; // drop the only strong reference
// After the next GC, ref.get() may return null.Las fugas de memoria siguen ocurriendo
Un recolector de basura te libera de los punteros colgantes y los dobles liberaciones, pero no de las fugas. Una fuga de memoria en Java es un objeto que ya no utilizas pero que aún es alcanzable desde una raíz, por lo que el GC debe conservarlo. El heap se llena, el GC se ejecuta cada vez más a menudo, y finalmente obtienes un OutOfMemoryError.
Las causas clásicas son todas "me olvidé de soltar":
- Una colección
static(caché, lista de listeners, mapa) a la que sigues añadiendo pero de la que nunca eliminas. Los campos estáticos son raíces GC, por lo que todo lo que alcanzan vive para siempre. - Listeners o callbacks registrados con un objeto de larga vida y nunca desregistrados.
- Claves dejadas en un
HashMapmucho después de ser necesarias, porque el mapa aún las referencia.
La solución no es una opción — es soltar las referencias cuando hayas terminado (eliminar de la colección, desregistrar el listener) o usar una estructura basada en WeakReference como WeakHashMap para que el GC pueda reclamar las entradas cuando nada más apunte a la clave.
finalize() está obsoleto y es poco fiable — la JVM puede ejecutarlo tarde o no ejecutarlo en absoluto. Para liberar archivos, sockets u otros recursos que no sean memoria de forma determinista, usa try-with-resources y AutoCloseable, no el recolector de basura.Elegir un recolector
La JVM HotSpot incluye varios recolectores con diferentes compromisos entre rendimiento (trabajo total realizado) y latencia (duración de las pausas). Se elige uno con una opción de JVM; el predeterminado desde Java 9 es G1.
| Recolector | Opción | Ideal para |
|---|---|---|
| G1 (predeterminado) | -XX:+UseG1GC | Latencia/rendimiento equilibrados, heaps grandes |
| Parallel | -XX:+UseParallelGC | Trabajos por lotes que priorizan el rendimiento bruto |
| ZGC | -XX:+UseZGC | Heaps muy grandes, pausas de menos de un milisegundo |
| Serial | -XX:+UseSerialGC | Heaps pequeños, un solo núcleo o contenedores |
# Pick a collector and set the heap size at launch:
java -XX:+UseG1GC -Xms256m -Xmx2g MyApp
# Print what the GC is doing, with timestamps:
java -Xlog:gc* MyAppUn ejemplo práctico
El programa siguiente hace observable el comportamiento del GC. Fija un objeto con una referencia fuerte, mantiene otro solo mediante una WeakReference, genera una oleada de basura de corta vida, luego solicita una recolección e informa sobre qué sobrevivió y cómo cambió el uso del heap.
Lo que se puede aprender de la ejecución:
- El referente débil imprime
trueantes de la recolección yfalsedespués, demostrando que unaWeakReferenceno mantiene vivo su objeto cuando no quedan referencias fuertes. - El array
keptmantenido con fuerza imprimesurvived: trueincluso después deSystem.gc(), porque es alcanzable desde una raíz GC y el recolector debe preservarlo. - Se asignan aproximadamente 300 MB de basura (
Bytes allocated as garbage: 307200000), pero el heap usado solo sube a unos 5 MB — los minor GC recuperan los arrays del bucle de corta vida tan rápido como se crean. Runtime.maxMemory()reporta el límite del heap (unos 256 MB aquí), establecido por-Xmx, mientras quetotalMemory() - freeMemory()es la porción usada en vivo que se mantiene cerca de 3–5 MB a lo largo de la ejecución.System.gc()es solo una sugerencia, pero en esta JVM sí se ejecuta: el heap usado baja y el referente débil inalcanzable es limpiado en lugar de persistir.