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?
- 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.
- jcmd: Ejecuta:
donde <pid> — el identificador del proceso de Java.jcmd <pid> VM.flags - 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.
GO TO FULL VERSION