Máquina virtual Java: GC generacionales

La mayoría de las JVM dividen el montón en tres generaciones: the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation). ¿Cuáles son las razones detrás de tal pensamiento?

Los estudios empíricos han demostrado que la mayoría de los objetos que se crean tienen una vida útil muy corta:

Fuente

https://www.oracle.com

Como puede ver, a medida que se asignan más y más objetos con el tiempo, la cantidad de bytes que sobreviven se reduce (en general). Los objetos Java tienen una alta tasa de mortalidad.

Veremos un ejemplo sencillo. La clase String en Java es inmutable. Esto significa que cada vez que necesite cambiar el contenido de un objeto String, debe crear un nuevo objeto por completo. Supongamos que realiza cambios en la cadena 1000 veces en un bucle como se muestra en el siguiente código:

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

En cada bucle, creamos un nuevo objeto de cadena y la cadena creada durante la iteración anterior se vuelve inútil (es decir, no está referenciada por ninguna referencia). La vida útil de ese objeto fue solo una iteración: el GC los recopilará en poco tiempo. Estos objetos de corta duración se mantienen en el área de la generación joven del montón. El proceso de recolección de objetos de la generación joven se llama recolección de basura menor y siempre causa una pausa de "detener el mundo".

A medida que la generación joven se llena, el GC realiza una recolección de basura menor. Los objetos muertos se descartan y los objetos vivos se trasladan a la generación anterior. Los subprocesos de la aplicación se detienen durante este proceso.

Aquí, podemos ver las ventajas que ofrece un diseño de tal generación. La generación joven es solo una pequeña parte del montón y se llena rápidamente. Pero el procesamiento lleva mucho menos tiempo que el que se tarda en procesar todo el montón. Entonces, las pausas de 'stop-theworld' en este caso son mucho más breves, aunque más frecuentes. Siempre debemos apuntar a pausas más breves sobre otras más largas, aunque puedan ser más frecuentes. Discutiremos esto en detalle en secciones posteriores de este tutorial.

La generación joven se divide en dos espacios: eden and survivor space. Los objetos que han sobrevivido durante la recolección de eden se trasladan al espacio de supervivientes, y los que sobreviven al espacio de supervivientes se trasladan a la generación anterior. La generación joven se compacta mientras se recolecta.

A medida que los objetos se trasladan a la generación anterior, eventualmente se llena y debe recolectarse y compactarse. Diferentes algoritmos adoptan diferentes enfoques para esto. Algunos de ellos detienen los subprocesos de la aplicación (lo que lleva a una larga pausa de 'detener el mundo' ya que la generación anterior es bastante grande en comparación con la generación joven), mientras que algunos de ellos lo hacen simultáneamente mientras los subprocesos de la aplicación siguen ejecutándose. Este proceso se llama GC completo. Dos de estos coleccionistas sonCMS and G1.

Analicemos ahora estos algoritmos en detalle.

GC en serie

es el GC predeterminado en las máquinas de clase cliente (máquinas de un solo procesador o JVM 32b, Windows). Por lo general, los GC tienen muchos subprocesos múltiples, pero el GC en serie no lo es. Tiene un único subproceso para procesar el montón y detendrá los subprocesos de la aplicación siempre que esté haciendo un GC menor o un GC mayor. Podemos ordenarle a la JVM que use este GC especificando la bandera:-XX:+UseSerialGC. Si queremos que utilice algún algoritmo diferente, especifique el nombre del algoritmo. Tenga en cuenta que la generación anterior se compacta por completo durante una GC importante.

Rendimiento GC

Este GC es predeterminado en máquinas JVM 64b y máquinas con varias CPU. A diferencia del GC en serie, utiliza varios subprocesos para procesar a las generaciones jóvenes y viejas. Debido a esto, el GC también se llamaparallel collector. Podemos ordenar a nuestra JVM que use este recopilador usando la bandera:-XX:+UseParallelOldGC o -XX:+UseParallelGC(para JDK 8 en adelante). Los subprocesos de la aplicación se detienen mientras se realiza una recolección de basura mayor o menor. Al igual que el colector en serie, compacta completamente a la generación joven durante una gran GC.

El GC de rendimiento recopila el YG y el OG. Cuando el edén se ha llenado, el recolector expulsa los objetos vivos de él hacia el OG o uno de los espacios de supervivientes (SS0 y SS1 en el siguiente diagrama). Los objetos muertos se descartan para liberar el espacio que ocupaban.

Antes de GC de YG

Después de GC de YG

Durante un GC completo, el colector de rendimiento vacía todo el YG, SS0 y SS1. Después de la operación, el OG contiene solo objetos vivos. Debemos tener en cuenta que los dos recopiladores anteriores detienen los subprocesos de la aplicación mientras procesan el montón. Esto significa largas pausas en el "mundo secundario" durante una CG importante. Los siguientes dos algoritmos apuntan a eliminarlos, a costa de más recursos de hardware:

Recopilador de CMS

Significa "barrido de marca concurrente". Su función es que utiliza algunos subprocesos en segundo plano para escanear periódicamente la generación anterior y deshacerse de los objetos muertos. Pero durante una GC menor, los subprocesos de la aplicación se detienen. Sin embargo, las pausas son bastante pequeñas. Esto convierte al CMS en un recopilador de pausa baja.

Este recopilador necesita tiempo de CPU adicional para escanear el montón mientras ejecuta los subprocesos de la aplicación. Además, los subprocesos de fondo simplemente recogen el montón y no realizan ninguna compactación. Pueden hacer que el montón se fragmente. A medida que esto continúa, después de cierto tiempo, el CMS detendrá todos los subprocesos de la aplicación y compactará el montón utilizando un solo subproceso. Utilice los siguientes argumentos de JVM para decirle a la JVM que utilice el recopilador de CMS:

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” como argumentos de JVM para indicarle que utilice el recopilador de CMS.

Antes de GC

Después de GC

Tenga en cuenta que la recopilación se realiza al mismo tiempo.

G1 GC

Este algoritmo funciona dividiendo el montón en varias regiones. Al igual que el recopilador de CMS, detiene los subprocesos de la aplicación mientras realiza una GC menor y utiliza subprocesos en segundo plano para procesar la generación anterior mientras mantiene los subprocesos de la aplicación en funcionamiento. Dado que dividió a la generación anterior en regiones, las sigue compactando mientras mueve objetos de una región a otra. Por tanto, la fragmentación es mínima. Puedes usar la bandera:XX:+UseG1GCpara decirle a su JVM que use este algoritmo. Al igual que CMS, también necesita más tiempo de CPU para procesar el montón y ejecutar los subprocesos de la aplicación al mismo tiempo.

Este algoritmo ha sido diseñado para procesar montones más grandes (> 4G), que se dividen en varias regiones diferentes. Algunas de esas regiones comprenden a la generación joven y el resto a la vieja. El YG se borra usando tradicionalmente: todos los subprocesos de la aplicación se detienen y todos los objetos que todavía están vivos para la generación anterior o el espacio de sobrevivientes.

Tenga en cuenta que todos los algoritmos de GC dividieron el montón en YG y OG, y usan un STWP para borrar el YG. Este proceso suele ser muy rápido.