W3docs

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 GCEjemplo
Variables localesUna referencia en la pila de un hilo en ejecución
Campos estáticosstatic final Logger LOG = ...
Hilos activosUn objeto Thread vivo
Referencias JNIObjetos 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 collection

El 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ónContieneRecolección
Young (Eden + 2 espacios Survivor)Objetos recién asignadosCon frecuencia, mediante un rápido minor GC
Old (Tenured)Objetos que sobrevivieron varios minor GCRaramente, mediante un major/full GC más lento
MetaspaceMetadatos 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 references

Puedes 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.

ReferenciaComportamiento del GC
Fuerte (el = ordinario)Nunca recolectada mientras sea alcanzable
SoftReferenceRecolectada solo cuando la memoria es escasa — ideal para cachés
WeakReferenceRecolectada en el próximo GC cuando no quedan referencias fuertes
PhantomReferenceSe 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 HashMap mucho 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.

Advertencia
Java no tiene destructores, y 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.

RecolectorOpciónIdeal para
G1 (predeterminado)-XX:+UseG1GCLatencia/rendimiento equilibrados, heaps grandes
Parallel-XX:+UseParallelGCTrabajos por lotes que priorizan el rendimiento bruto
ZGC-XX:+UseZGCHeaps muy grandes, pausas de menos de un milisegundo
Serial-XX:+UseSerialGCHeaps 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* MyApp

Un 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.

java— editable, runs on the server

Lo que se puede aprender de la ejecución:

  • El referente débil imprime true antes de la recolección y false después, demostrando que una WeakReference no mantiene vivo su objeto cuando no quedan referencias fuertes.
  • El array kept mantenido con fuerza imprime survived: true incluso después de System.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 que totalMemory() - 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.

Práctica

Práctica
Un objeto es referenciado únicamente por una variable local que acaba de salir del ámbito y por nada más. ¿Qué lo hace elegible para la recolección de basura?
Un objeto es referenciado únicamente por una variable local que acaba de salir del ámbito y por nada más. ¿Qué lo hace elegible para la recolección de basura?
Was this page helpful?