W3docs

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ónQué vive aquíCon qué frecuencia se recolecta
EdenObjetos recién asignadosEn cada GC menor
Survivor (S0/S1)Objetos que sobrevivieron un GC menorEn cada GC menor
Old (tenured)Objetos de larga vida, promovidosGC mayor / completo
MetaspaceMetadatos 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 compaction

La siguiente tabla es el modelo mental que debes llevar contigo:

RecolectorFortalezaComportamiento de pausaUso típico
SerialEl más simple, baja huellaStop-the-world, hilo únicoHeaps pequeños, contenedores, CLIs
ParallelMayor rendimientoStop-the-world, muchos hilosProcesamiento por lotes / de datos
G1Equilibrado, predecibleMayormente concurrente, pausa objetivoUso general predeterminado
ZGCLatencia muy bajaSub-milisegundo, concurrenteHeaps de varios GB a TB
ShenandoahLatencia muy bajaPausas independientes del tamaño del heapServicios 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.832ms

Esa 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 false

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

java— editable, runs on the server

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 variable garbage, por lo que la WeakReference puede 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 que garbage = null lo hizo inalcanzable y System.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: el survivor, 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.

Práctica

Práctica
¿Qué propiedad única determina si el recolector de basura conservará un objeto, independientemente del algoritmo que se use?
¿Qué propiedad única determina si el recolector de basura conservará un objeto, independientemente del algoritmo que se use?
Was this page helpful?