Algoritmos GC de Java
Compara los principales recolectores de basura de Java — Serial, Parallel, G1, ZGC y Shenandoah — y sus compromisos.
La JVM te libera de liberar memoria manualmente: un recolector de basura (GC) se ejecuta en segundo plano, encuentra los objetos que tu programa ya no puede alcanzar y recupera su espacio. Pero "el recolector de basura" no es una sola cosa. La JVM HotSpot incluye varios recolectores, cada uno con un equilibrio diferente entre rendimiento (cuánto CPU va a tu aplicación en lugar de al GC), latencia (cuánto tiempo se pausa la aplicación mientras el GC trabaja) y huella (cuánta memoria cuesta el propio overhead del recolector).
Elegir bien importa. Un trabajo por lotes que procesa números toda la noche quiere el máximo rendimiento y no le preocupan las pausas; un sistema de trading o un servidor web quiere las pausas más cortas posibles, aunque el rendimiento total baje un poco. Este capítulo compara los recolectores de producción y muestra lo que todos tienen en común: un objeto vive exactamente mientras sea alcanzable.
Cómo los recolectores deciden qué conservar
Cada recolector HotSpot responde la misma pregunta — qué objetos están todavía en uso — de la misma manera: traza la alcanzabilidad desde un conjunto de raíces GC (variables locales en la pila, campos estáticos, hilos activos). Cualquier objeto alcanzable desde una raíz está vivo; todo lo demás es basura. El ámbito y la edad son irrelevantes; solo cuenta la alcanzabilidad. Para una visión más amplia de cómo esto encaja en el entorno de ejecución, consulta Java Garbage Collection y Stack vs Heap.
public class Reachability {
public static void main(String[] args) {
String a = new String("kept"); // reachable via local variable 'a'
String b = new String("dropped"); // reachable via 'b'...
b = null; // ...until now: "dropped" is unreachable
System.out.println(a); // 'a' is still a GC root reference
}
}En el momento en que se ejecuta b = null, el objeto "dropped" no tiene camino desde ninguna raíz y se vuelve elegible para la recolección. El recolector puede recuperarlo de inmediato, mucho más tarde, o — si el programa termina primero — nunca. Nunca llamas a free; simplemente dejas de referenciar.
Distribución del heap generacional
La mayoría de los objetos Java mueren jóvenes. Los recolectores aprovechan esto con un heap generacional: los nuevos objetos llegan a la generación joven (Eden más dos espacios survivor), y los objetos que sobreviven varias recolecciones son promovidos a la generación vieja. Recolectar frecuentemente la generación joven, pequeña y llena de basura, es económico; la generación vieja y grande se recolecta con mucha menos frecuencia.
| Región | Qué vive aquí | Con qué frecuencia se recolecta |
|---|---|---|
| Eden | Objetos recién asignados | En cada GC menor |
| Survivor (S0/S1) | Objetos que sobrevivieron un GC menor | En cada GC menor |
| Old (tenured) | Objetos de larga vida, promovidos | GC mayor / completo |
| Metaspace | Metadatos de clase (fuera del heap) | Al descargar clases |
Un GC menor limpia la generación joven y es rápido; un GC mayor o completo toca la generación vieja y es la fuente de las largas pausas que tanto preocupan.
Comparación de los recolectores
HotSpot te permite elegir un recolector con una sola bandera, y cada uno está ajustado para un objetivo diferente. Rara vez cambias el algoritmo en el código — lo configuras en la línea de comandos.
java -XX:+UseSerialGC MyApp # single-threaded, tiny heaps
java -XX:+UseParallelGC MyApp # throughput-oriented, multi-threaded
java -XX:+UseG1GC MyApp # balanced, the default since Java 9
java -XX:+UseZGC MyApp # sub-millisecond pauses, huge heaps
java -XX:+UseShenandoahGC MyApp # low pause, concurrent compactionLa siguiente tabla es el modelo mental que debes llevar contigo:
| Recolector | Fortaleza | Comportamiento de pausa | Uso típico |
|---|---|---|---|
| Serial | El más simple, baja huella | Stop-the-world, hilo único | Heaps pequeños, contenedores, CLIs |
| Parallel | Mayor rendimiento | Stop-the-world, muchos hilos | Procesamiento por lotes / de datos |
| G1 | Equilibrado, predecible | Mayormente concurrente, pausa objetivo | Uso general predeterminado |
| ZGC | Latencia muy baja | Sub-milisegundo, concurrente | Heaps de varios GB a TB |
| Shenandoah | Latencia muy baja | Pausas independientes del tamaño del heap | Servicios responsivos |
G1 ("Garbage-First") es el predeterminado desde Java 9 en adelante. Divide el heap en regiones de igual tamaño y recolecta primero las regiones con más basura, apuntando a un objetivo de tiempo de pausa que estableces con -XX:MaxGCPauseMillis=200.
Concurrente vs stop-the-world
El eje crucial es cuándo los hilos de la aplicación tienen que detenerse. Los recolectores stop-the-world (STW) (Serial, Parallel) pausan todos los hilos de la aplicación mientras trabajan — simples y de alto rendimiento, pero la pausa crece con el heap. Los recolectores concurrentes (ZGC, Shenandoah y la mayor parte de G1) hacen la mayor parte de su trabajo mientras tus hilos siguen ejecutándose, por lo que las pausas se mantienen cortas incluso cuando los heaps alcanzan gigabytes o terabytes.
# See exactly what the collector is doing and how long it pauses
java -Xlog:gc -XX:+UseG1GC MyApp
# Sample output line:
# [0.412s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 24M->5M(64M) 1.832msEsa línea de log vale la pena decodificarla: 24M->5M(64M) significa que el heap usado bajó de 24 MB a 5 MB de un total de 64 MB, y la aplicación se pausó durante 1.832ms. Leer la salida de -Xlog:gc es la habilidad de ajuste de GC más útil — mide antes de cambiar cualquier bandera.
Observar la recolección desde el código
No puedes invocar directamente un algoritmo específico desde Java, pero sí puedes observar que la recolección ocurre. Una WeakReference te permite mantener un puntero que no mantiene vivo su objetivo, por lo que puedes preguntar "¿ya fue recolectado este objeto?". La clase Runtime informa el uso del heap, y System.gc() es una sugerencia — nunca un comando — para ejecutar una recolección ahora.
import java.lang.ref.WeakReference;
WeakReference<byte[]> ref = new WeakReference<>(new byte[1024]);
System.out.println("Before GC: " + (ref.get() != null)); // true
System.gc();
System.out.println("After GC: " + (ref.get() != null)); // usually falseEl ejemplo ejecutable a continuación une estas piezas: asigna una oleada de basura, mantiene un superviviente alcanzable, observa un objeto inalcanzable a través de una referencia débil y mide el heap antes y después de una recolección.
Lo que se puede extraer de la ejecución:
- El paso 2 imprime
true: el objeto observado todavía es fuertemente alcanzable a través de la variablegarbage, por lo que laWeakReferencepuede leerlo. - El paso 3 informa el heap usado después de 300.000 asignaciones de corta duración. El número exacto varía de ejecución en ejecución — un GC menor puede haber barrido buena parte de esa oleada en medio del bucle — pero crearla es precisamente el desgaste de la generación joven que todo recolector generacional está construido para manejar a bajo costo.
- El paso 4 imprime
true, confirmando que el objeto observado fue recuperado una vez quegarbage = nulllo hizo inalcanzable ySystem.gc()desencadenó una recolección — prueba de que la pérdida de alcanzabilidad, no el salir del ámbito, es lo que libera memoria. - El paso 5 imprime
true: elsurvivor, todavía referenciado por una variable local activa (una raíz GC), cruza la recolección sin ser tocado. - El paso 6 muestra el heap usado cayendo cerca de la línea base, demostrando que el recolector devuelve el espacio recuperado para su reutilización en lugar de que el programa lo pierda.
Para más información sobre los tipos de referencia usados aquí — fuertes, suaves, débiles y fantasma — consulta Java References.