CodeGym /Cursos /JAVA 25 SELF /Recolectores de basura: G1, ZGC, Shenandoah, comparativa

Recolectores de basura: G1, ZGC, Shenandoah, comparativa

JAVA 25 SELF
Nivel 64, Lección 1
Disponible

1. Introducción al recolector de basura (GC)

Si alguna vez has programado en C o C++, seguramente te habrás encontrado con la necesidad de liberar memoria manualmente usando free() o delete. En Java todo es mucho más sencillo: creas un objeto con new y no necesitas eliminarlo — de ello se encarga un «conserje» especial llamado recolector de basura (Garbage Collector, GC).

El GC es parte de la JVM y libera automáticamente la memoria ocupada por objetos a los que ya no hay referencias. Gracias a ello, los desarrolladores de Java no tienen que preocuparse por olvidar liberar memoria (y provocar una fuga) o, por el contrario, eliminar por accidente un objeto que todavía se necesita (y causar un crash).

Pero, como cualquier conserje, el GC no es perfecto: a veces puede intervenir en el peor momento y hacer una «limpieza general» (Stop-the-World), o no trabajar tan rápido como nos gustaría. Por eso en la JVM existen varias implementaciones del recolector de basura — y elegir la adecuada puede afectar notablemente al rendimiento de la aplicación.

Tipos principales de recolectores de basura

Serial GC

  • Serial GC — el recolector de basura más simple y antiguo.
  • Funciona en un solo hilo.
  • Detiene todos los demás hilos durante la limpieza (Stop-the-World).
  • Adecuado para aplicaciones pequeñas sin mucha concurrencia.
  • Se activa con la opción: -XX:+UseSerialGC

Parallel GC

  • Parallel GC (también «Throughput Collector»).
  • Usa varios hilos para la recolección.
  • Orientado a maximizar el throughput.
  • Sigue realizando la limpieza con pausas Stop-the-World, pero más rápido que Serial.
  • Adecuado para aplicaciones de servidor donde pequeñas pausas no son críticas.
  • Se activa con la opción: -XX:+UseParallelGC

CMS (Concurrent Mark Sweep)

  • CMS — GC obsoleto, aunque durante mucho tiempo popular, que minimiza las pausas.
  • Trabaja parcialmente en paralelo con la aplicación, reduciendo el tiempo de parada.
  • Más complejo de configurar, con sobrecostes.
  • Marcado como obsoleto desde Java 9 (deprecated).
  • Se activa con la opción: -XX:+UseConcMarkSweepGC

G1 (Garbage First)

  • G1 GC — recolector moderno por defecto (desde Java 9).
  • Equilibra pausas mínimas y rendimiento.
  • Divide el heap en muchas regiones pequeñas (modelo por regiones).
  • Puede recolectar basura selectivamente por regiones sin tocar todo el heap.
  • Permite fijar una pausa máxima objetivo, por ejemplo -XX:MaxGCPauseMillis=200.
  • Opción de activación: -XX:+UseG1GC (normalmente no hace falta, porque G1 es el predeterminado).

ZGC y Shenandoah

  • ZGC y Shenandoah — recolectores modernos de baja latencia.
  • Objetivo: pausas mínimas (milisegundos), incluso con heaps enormes (hasta terabytes).
  • Funcionan prácticamente en paralelo con la aplicación.
  • Requieren Java 11+ (ZGC) o Java 12+ (Shenandoah).
  • Adecuados para sistemas críticos en latencia (bolsas, fintech, analítica en tiempo real).
  • Opciones de activación: -XX:+UseZGC o -XX:+UseShenandoahGC

3. Principios de funcionamiento de los GC modernos

Generación joven y vieja (Young/Old Generation)

La JVM divide el heap en dos grandes partes:

Generación joven (Young Generation): aquí van todos los objetos nuevos. La limpieza ocurre con frecuencia y rápidamente ( Minor GC ).

Generación vieja (Old Generation, Tenured): aquí se mueven los objetos que «han sobrevivido» a varias limpiezas en la generación joven. La limpieza aquí es menos frecuente pero más lenta ( Major/Full GC ).

¿Por qué? La mayoría de los objetos en Java viven muy poco tiempo (por ejemplo, cadenas temporales, colecciones dentro de un método). Por eso se puede limpiar la generación joven de forma rápida y frecuente sin tocar la vieja.

Minor GC

  • Limpia solo la generación joven.
  • Rápido, con pausa corta.
  • No afecta a los objetos viejos.

Major (Full) GC

  • Limpia todo el heap (tanto la generación joven como la vieja).
  • Puede tardar mucho tiempo (segundos o más en heaps grandes).
  • Suele ir acompañado de una pausa larga de la aplicación.

¿Cómo decide el GC qué objetos eliminar?

El GC busca objetos «vivos» empezando por el conjunto raíz (root set): variables locales en las pilas de los hilos, campos estáticos, parámetros de métodos, etc. Todo lo que sea alcanzable se considera vivo. El resto es basura.

4. Comparación de los recolectores modernos: G1, ZGC, Shenandoah

Veamos en qué se diferencian los GC más modernos y populares. Para ello, una tabla ilustrativa:

Recolector Objetivo principal Modelo de memoria Pausas mínimas Escalabilidad Soporte Cuándo usarlo
G1 Equilibrio pausas/velocidad Regiones ~10–200 ms Hasta cientos de GB Java 9+ (predeterminado) La mayoría de aplicaciones de servidor
ZGC Pausa mínima Regiones, «etiquetas de color» <10 ms Hasta terabytes Java 11+ Tiempo real, crítico en latencia
Shenandoah Pausa mínima Regiones, «etiquetas de color» <10 ms Hasta terabytes Java 12+ (Red Hat) Tiempo real, crítico en latencia

G1 GC: Garbage First

  • Divide el heap en muchas regiones (normalmente de 1–32 MB cada una).
  • Durante la limpieza, selecciona primero las regiones con más basura («garbage first»).
  • Puede recolectar solo una parte del heap, no todo de golpe.
  • Permite establecer una pausa objetivo: -XX:MaxGCPauseMillis=200.
  • Adecuado para equilibrar velocidad y pausas; se usa por defecto desde Java 9.

Ejemplo de activación (si estuviera desactivado):

java -XX:+UseG1GC -jar myapp.jar

ZGC: Z Garbage Collector

  • Experimental en Java 11, estable desde Java 15.
  • Apenas detiene la aplicación: las pausas suelen ser <10 ms, incluso con 1–2 TB de heap.
  • Usa «etiquetas de color» (coloring) y punteros especiales.
  • Requiere una JVM de 64 bits; no funciona en sistemas de 32 bits.
  • Compatible con Linux, macOS y Windows.

Ejemplo de activación:

java -XX:+UseZGC -jar myapp.jar

Shenandoah

  • Desarrollado por Red Hat; objetivos similares a ZGC.
  • Pausas mínimas, trabajo paralelo activo con la aplicación.
  • Soporte en Linux y Windows; forma parte de algunas compilaciones de OpenJDK.
  • Usa técnicas parecidas, pero con algoritmos internos diferentes.

Ejemplo de activación:

java -XX:+UseShenandoahGC -jar myapp.jar

Comparativa visual

graph TD
    A[Generación joven] -->|Minor GC| B[Generación vieja]
    B -->|Major GC| C[GC Pause]
    D[G1: regiones] --> E[Regiones seleccionadas]
    F[ZGC/Shenandoah: regiones] --> G[Recolección paralela]

5. Práctica: cómo averiguar y cambiar el GC

¿Cómo saber qué GC se está usando?

  1. Logs de la JVM: Inicia la aplicación con los parámetros -Xlog:gc* (Java 9+) o -verbose:gc (hasta Java 8). En los logs se verá qué GC se utiliza y con qué frecuencia se producen las pausas.
  2. jcmd: Ejecuta:
    jcmd <pid> VM.flags
    
    donde <pid> — el identificador del proceso de Java.
  3. jvisualvm: En la sección «Monitoring» puedes ver el tipo de GC.

¿Cómo cambiar el GC para tu aplicación?

Añade la opción necesaria al iniciar la aplicación Java:

G1 GC (por defecto, se puede indicar explícitamente):

java -XX:+UseG1GC -jar myapp.jar

ZGC:

java -XX:+UseZGC -jar myapp.jar

Shenandoah:

java -XX:+UseShenandoahGC -jar myapp.jar

¿Cómo establecer el tamaño del heap y las pausas?

  • Tamaño máximo del heap: -Xmx2G
  • Tamaño mínimo del heap: -Xms512M
  • Para G1: pausa objetivo — -XX:MaxGCPauseMillis=200

Ejemplo de ejecución completa:

java -Xms512M -Xmx2G -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar myapp.jar

6. Criterios para elegir el GC según la tarea

Cuándo elegir G1

  • En la mayoría de aplicaciones de servidor y de escritorio — una excelente opción por defecto.
  • Funciona bien con heaps desde cientos de megabytes hasta cientos de gigabytes.
  • Equilibra velocidad y pausas.

Cuándo elegir ZGC o Shenandoah

  • Si la aplicación es sensible a la latencia (latency‑critical: bolsas, juegos en línea, analítica en tiempo real).
  • Si el heap es enorme (cientos de gigabytes o más).
  • Si solo se admiten pausas mínimas (milisegundos).
  • Se requiere Java 11+ (ZGC) o Java 12+ (Shenandoah).

Cuándo basta con Parallel GC

  • Para aplicaciones pequeñas donde importa maximizar el throughput y las pausas no son críticas.
  • Para procesamiento por lotes (batch), donde se puede «tolerar» una parada por Full GC.

7. Ejemplo: comparación del comportamiento del GC en una aplicación sencilla

Una pequeña aplicación que genera muchos objetos temporales (simulación del procesamiento de pedidos):

public class GCSimulator {
    public static void main(String[] args) {
        while (true) {
            // Creamos 100 000 objetos en cada ciclo
            for (int i = 0; i < 100_000; i++) {
                String s = new String("Order-" + i);
            }
            // Hacemos una pequeña pausa
            try { Thread.sleep(100); } catch (InterruptedException e) {}
        }
    }
}

Ejecuta esto con distintos GC y observa los logs:

java -Xmx256M -XX:+UseG1GC -Xlog:gc* GCSimulator
java -Xmx256M -XX:+UseZGC -Xlog:gc* GCSimulator

¿Qué verás?
G1 hará pausas frecuentes pero cortas. ZGC/Shenandoah — pausas aún más cortas, pero pueden ocurrir con mayor frecuencia. Parallel GC — pausas más largas pero menos frecuentes.

8. Errores típicos y matices al trabajar con el GC

Error n.º 1: esperar que el GC resuelva todos los problemas de memoria. El GC no es una varita mágica. Si mantienes referencias a objetos innecesarios, ningún GC te ayudará — habrá una fuga de memoria.

Error n.º 2: llamada forzada a System.gc(). La JVM sabe mejor cuándo recolectar basura. Un GC forzado puede provocar una pausa larga y reducir el rendimiento.

Error n.º 3: ignorar los logs del GC. Si no miras los logs del GC, puedes no darte cuenta de que tu aplicación se «congela» regularmente por Full GC.

Error n.º 4: uso de GC obsoletos. Por ejemplo, CMS ya no se desarrolla. Es mejor pasar a G1 o a recolectores modernos de baja latencia.

Error n.º 5: elegir un GC inadecuado para la tarea. Si tienes una aplicación crítica en latencia y usas Parallel GC, espera pausas largas. Si tienes procesamiento por lotes (batch) y activas ZGC, añadirás sobrecoste innecesario.

1
Tarea
JAVA 25 SELF, nivel 64, lección 1
Bloqueada
Descubriendo el recolector de basura en uso – Detective JVM 🕵️
Descubriendo el recolector de basura en uso – Detective JVM 🕵️
1
Tarea
JAVA 25 SELF, nivel 64, lección 1
Bloqueada
Cambio del recolector de basura y ajuste de pausas – Perfeccionista: Ajustes de rendimiento ⚙️
Cambio del recolector de basura y ajuste de pausas – Perfeccionista: Ajustes de rendimiento ⚙️
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION