Recolección de basura en Java: qué es y cómo funciona en la JVM

Recolección de basura en Java: qué es y cómo funciona en la JVM

@programacion
https://unsplash.com/photos/TEemXOpR3cQ

¿Qué es la recolección de basura en Java?

La recolección de basura es el proceso de recuperar la memoria de tiempo de ejecución completa mediante la destrucción de objetos no utilizados.

En lenguajes como C y C++, el programador es responsable tanto de crear como de destruir objetos. A veces, el programador puede olvidarse de destruir objetos inútiles y la memoria asignada a ellos no se libera. Cada vez se consume más memoria del sistema y finalmente no se asigna más. Tales aplicaciones sufren de "fugas de memoria".

Después de cierto punto, ya no hay suficiente memoria para crear nuevos objetos y el programa finaliza de manera anormal debido a OutOfMemoryErrors.

En C++, puede usar el método para la recolección de basura delete(), mientras que en C, puede usar el método free(). En Java, la recolección de basura ocurre automáticamente durante la duración del programa. Esto elimina la necesidad de asignar memoria y, por lo tanto, evita fugas.

La recolección de basura en Java es el proceso mediante el cual los programas Java administran la memoria automáticamente. Los programas de Java se compilan en un código de bytes que se ejecuta en la máquina virtual de Java (JVM).

Cuando los programas Java se ejecutan en la JVM, los objetos se crean en el Heap, que es la parte de la memoria que se les asigna. 

Mientras se ejecuta una aplicación Java, se crean y ejecutan nuevos objetos dentro de ella. Al final, algunos objetos ya no son necesarios. Podemos decir que en un momento dado, la memoria del Heap consta de dos tipos de objetos.

  • Live  : estos objetos se usan, se hace referencia a ellos desde otro lugar.
  • Deap  : estos objetos no se usan en ningún otro lugar, no hay referencias a ellos.

El recolector de elementos no utilizados encuentra estos objetos no utilizados y los elimina para liberar memoria.

Cómo desreferenciar un objeto en Java

El propósito principal de la recolección de elementos no utilizados es liberar memoria en Heap al destruir objetos que no contienen una referencia. Cuando no se hace referencia a un objeto, se supone que está muerto y que ya no se necesita. Por lo tanto, se puede recuperar la memoria ocupada por el objeto.

Hay varias formas de eliminar las referencias a un objeto y convertirlo en un candidato para la recolección de elementos no utilizados. Éstos son algunos de ellos.

Hacer referencia nula

Student student = new Student();
student = null;

Asignar un enlace a otro objeto

Student studentOne = new Student();
Student studentTwo = new Student();
studentOne = studentTwo; // Т

Usar objeto anónimo

register(new Student());

¿Cómo funciona la recolección de basura en Java?

La recolección de basura en Java es un proceso automático. El programador no necesita marcar explícitamente los objetos para eliminarlos.

La recolección de basura se realiza en la JVM. Cada JVM puede implementar su propia versión de recolección de basura. Sin embargo, el recopilador debe cumplir con la especificación estándar de JVM para tratar con objetos presentes en la memoria del Heap para marcar o identificar objetos inalcanzables y destruirlos mediante compactación.

¿Cuáles son las fuentes para la recolección de basura en Java?

Los recolectores de basura trabajan con el concepto de raíces de recolección de basura (GC Roots) para identificar objetos vivos y muertos.

Ejemplos de tales raíces.

  • Clases cargadas por el cargador de clases del sistema (no cargadores de clases personalizados).
  • Transmisiones en vivo.
  • Variables locales y parámetros de los métodos que se están ejecutando actualmente.
  • Variables locales y parámetros del método JNI.
  • Referencia global a JNI.
  • Objetos utilizados como monitor para la sincronización.
  • Objetos retenidos de la recolección de elementos no utilizados de JVM para sus propios fines.

El recolector de basura recorre todo el gráfico de objetos en la memoria, comenzando en estas raíces y siguiendo las referencias a otros objetos.

Pasos de recolección de basura en Java

La implementación estándar de la recolección de elementos no utilizados consta de tres pasos.

Marcar objetos como vivos

En esta etapa, el GC (recolector de basura) identifica todos los objetos vivos en la memoria atravesando el gráfico de objetos.

Cuando el GC visita un objeto, lo marca como disponible y, por lo tanto, vivo. Todos los objetos a los que no se puede acceder desde las raíces del GC se consideran candidatos para la recolección de elementos no utilizados.

Limpiar objetos muertos

Después de la fase de marcado, el espacio de la memoria está ocupado por objetos vivos (visitados) o muertos (no visitados). La fase de limpieza libera los fragmentos de memoria que contienen estos objetos muertos.

Disposición compacta de los objetos restantes en la memoria

Los objetos muertos que se retiraron durante la fase anterior no estaban necesariamente uno al lado del otro. Entonces corre el riesgo de obtener espacio de memoria fragmentado.

La memoria se puede compactar cuando el recolector de basura elimina los objetos muertos. El resto se ubicará en un bloque contiguo al comienzo del Heap.

El proceso de compactación facilita la asignación secuencial de memoria para nuevos objetos.

¿Qué es la recolección de basura generacional?

Los recolectores de basura en Java implementan una estrategia de recolección de basura generacional que clasifica los objetos por edad.

Tener que marcar y compactar todos los objetos en la JVM es ineficiente. A medida que se asignan más y más objetos, su lista crece, lo que conduce a un aumento en el tiempo de recolección de elementos no utilizados. El análisis empírico de las aplicaciones ha demostrado que la mayoría de los objetos en Java son de corta duración.

En el ejemplo anterior, el eje y muestra la cantidad de bytes asignados y el eje x muestra la cantidad de bytes asignados a lo largo del tiempo. Como puede ver, con el tiempo, cada vez menos objetos retienen la memoria asignada.

La mayoría de los objetos viven vidas muy cortas, lo que corresponde a los valores más altos del lado izquierdo del gráfico. Esta es la razón por la que Java clasifica los objetos por generación y la recolección de elementos no utilizados en consecuencia.

El área de memoria de pila en la JVM se divide en tres secciones:

Generación más joven

Los objetos recién creados comienzan en la generación más joven. La generación más joven se subdivide en dos categorías.

  • Eden Space  : todos los objetos nuevos comienzan aquí y se les asigna memoria inicial.
  • Survivor Spaces (FromSpace y ToSpace)  : los objetos se mueven aquí desde Eden después de sobrevivir a un ciclo de recolección de basura.

El proceso en el que los objetos se recolectan como basura de la generación más joven se denomina evento de recolección de basura menor .

Cuando el espacio de Eden se llena de objetos, se realiza una pequeña recolección de basura. Todos los objetos muertos se eliminan y todos los vivos se mueven a uno de los dos espacios restantes. El GC pequeño también verifica los objetos en el espacio de supervivientes y los mueve a otro (siguiente) espacio de supervivientes.

Tome la siguiente secuencia como ejemplo.

  1. Hay objetos de ambos tipos (vivos y muertos) en el Edén.
  2. Se produce un pequeño GC: todos los objetos muertos se eliminan de Eden. Todos los objetos vivos se mueven al espacio-1 ( FromSpace ). Eden y space-2 ahora están vacíos.
  3. Se crean y agregan nuevos objetos a Eden. Algunos objetos en Eden y space-1 se vuelven muertos.
  4. Se produce un pequeño GC: todos los objetos muertos se eliminan de Eden y space-1. Todos los objetos vivos se mueven al espacio-2 (ToSpace). Eden y space-2 están vacíos de nuevo.

Así, en cualquier momento, uno de los espacios de supervivientes siempre está vacío. Cuando los sobrevivientes alcanzan un cierto umbral para moverse a través de los espacios de sobrevivientes, avanzan a una generación anterior.

Puede usar la bandera para establecer el tamaño de la generación joven -Xmn.

Vieja generación

Los objetos de larga vida finalmente pasan de la generación más joven a la más antigua. También se conoce como la generación normal y contiene objetos que se han dejado en Survivor Spaces durante mucho tiempo.

El umbral de vida útil de un objeto determina cuántos ciclos de recolección de elementos no utilizados puede sobrevivir antes de pasar a la generación anterior.

El proceso cuando los objetos se envían a la basura desde la generación anterior se denomina evento principal de recolección de basura .

Puede utilizar los indicadores y para establecer el -Xmstamaño de memoria de almacenamiento dinámico inicial y máximo -Xmx.

Debido a que Java utiliza la recolección de basura generacional, cuantos más eventos de recolección de basura experimenta un objeto, más se mueve en el Heap. Comienza en la generación más joven y eventualmente termina en la generación regular si vive lo suficiente.

Para entender la promoción de objetos entre espacios y generaciones, considere el siguiente ejemplo.

Cuando se crea un objeto, primero se coloca en el espacio Edén de la generación joven. Tan pronto como se produce una pequeña recolección de basura, los objetos vivos de Eden se trasladan a FromSpace. Cuando ocurre la próxima recolección de basura menor, los objetos vivos tanto de Eden como del espacio se mueven a ToSpace.

Este ciclo continúa un cierto número de veces. Si el objeto todavía está "en servicio" después de este punto, el siguiente ciclo de recolección de elementos no utilizados lo moverá al espacio de una generación más antigua.

generación permanente

Los metadatos, como clases y métodos, se almacenan en la generación persistente. La JVM lo llena en tiempo de ejecución en función de las clases utilizadas por la aplicación. Las clases que ya no se usan pueden pasar de generación permanente a basura.

Puede utilizar las banderas y para establecer el tamaño inicial y máximo de la -XX:PermGengeneración permanente -XX:MaxPermGen.

espacio meta

A partir de Java 8, el espacio de generación permanente (PermGen) se reemplaza por el espacio de memoria MetaSpace. La implementación difiere de PermGen: este espacio de almacenamiento dinámico ahora se cambia automáticamente.

Esto evita el problema de falta de memoria de la aplicación que se produce debido al tamaño limitado del espacio de almacenamiento dinámico de PermGen. La memoria del metaespacio se puede recolectar como basura, y las clases que ya no están en uso se limpiarán automáticamente cuando el metaespacio alcance su tamaño máximo.

Tipos de recolectores de basura en la máquina virtual de Java

La recolección de basura mejora la eficiencia de la memoria en Java porque los objetos sin referencia se eliminan de la memoria del Heap para dejar espacio para nuevos objetos.

La Máquina Virtual Java tiene ocho tipos de recolectores de basura. Consideremos cada uno de ellos en detalle.

GC en serie

Esta es la implementación de GC más simple. Está destinado a aplicaciones pequeñas que se ejecutan en entornos de subproceso único. Todos los eventos de recolección de basura se ejecutan secuencialmente en el mismo subproceso. La compactación se realiza después de cada recolección de basura.

La ejecución del recopilador da como resultado un evento de "parada mundial" en el que se suspende toda la aplicación. Dado que toda la aplicación se congela durante la recolección de basura, no debe recurrir a esto en la vida real si desea mantener los retrasos lo más bajos posible.

El argumento de JVM para usar el recolector de basura secuencial -XX:+UseSerialGC.

GC paralelo

El recolector de elementos no utilizados paralelo está diseñado para aplicaciones con conjuntos de datos medianos a grandes que se ejecutan en hardware multiprocesador o multihilo. Esta es la implementación predeterminada de GC y también se conoce como recopilador de rendimiento.

Varios subprocesos están destinados a la recolección de basura pequeña en la generación joven. El único subproceso está ocupado con la recolección de basura principal en la generación anterior.

Ejecutar un GC paralelo también hace que el mundo se "detenga" y la aplicación se cuelga. Esto es más apropiado para un entorno de subprocesos múltiples donde se deben completar muchas tareas y las pausas largas son aceptables, como cuando se ejecuta un trabajo por lotes.

Argumento de JVM para usar el recolector de basura paralelo: -XX:+UseParallelGC.

Antiguo GC paralelo

Esta es la versión predeterminada de Parallel GC desde Java 7u4. Esto es lo mismo que GC en paralelo, excepto que utiliza varios subprocesos tanto para la generación joven como para la generación anterior.

Argumento de JVM para usar el viejo recolector de basura simultáneo: -XX:+UseParallelOldGC.

CMS (marcado y raspado en paralelo) GC

También conocido como recogedor paralelo de descanso bajo. Para una recolección de basura pequeña, varios subprocesos están involucrados, y esto sucede a través del mismo algoritmo que en un recolector paralelo. La recolección de elementos no utilizados principal tiene múltiples subprocesos, al igual que el antiguo GC paralelo, pero el CMS se ejecuta simultáneamente con los procesos de la aplicación para minimizar los eventos de "parada mundial".


Debido a esto, el recopilador de CMS consume más CPU que otros recopiladores. Si tiene la capacidad de asignar más CPU para mejorar el rendimiento, entonces es preferible un CMS a un simple recopilador paralelo. El CMS GC no se compacta.

Argumento de JVM para usar el recolector de basura de barrido de etiquetas paralelo: -XX:+UseConcMarkSweepGC.

G1 (basura primero) GC

G1GC se concibió como un reemplazo para CMS y se desarrolló para aplicaciones de subprocesos múltiples que se caracterizan por grandes tamaños de almacenamiento dinámico (superiores a 4 GB). Es paralelo y competitivo como un CMS, pero bajo el capó funciona de manera muy diferente a los antiguos recolectores de basura.

Aunque G1 también opera de forma generacional, no tiene espacios separados para las generaciones más jóvenes y mayores. En cambio, cada generación es un conjunto de regiones, lo que permite flexibilidad para cambiar el tamaño de la generación más joven.

G1 divide el Heap en un conjunto de áreas del mismo tamaño (de 1 MB a 32 MB, según el tamaño del Heap) y las escanea en varios subprocesos. El área durante la ejecución del programa puede volverse repetidamente tanto vieja como joven.

Una vez completada la fase de marcado, G1 sabe qué áreas contienen la mayor cantidad de basura. Si el usuario está interesado en minimizar las pausas, G1 solo puede seleccionar algunas áreas. Si el tiempo de pausa no es importante para el usuario, o si el límite de tiempo de pausa se establece en un valor alto, G1 repasará más áreas.

Dado que el GC G1 identifica las regiones con la mayor cantidad de basura y realiza la recolección de basura primero en esas regiones, se denomina "Garbage First".

Además de las áreas de Eden , Survivors y Old Memory , hay otros dos tipos presentes en G1GC.

  • Humongous (Enorme)  : para objetos grandes (más del 50 % del tamaño del Heap).
  • Disponible (Available ): espacio no utilizado o no asignado.

El argumento de JVM para usar el recolector de basura G1 es: -XX:+UseG1GC.

Recolector de basura épsilon

Epsilon es un recolector de basura que se lanzó como parte de JDK 11. Maneja la asignación de memoria pero no implementa ningún mecanismo real de recuperación de memoria. Una vez que se agota el Heap disponible, la JVM sale.

Se puede utilizar para aplicaciones que son sensibles a la latencia ultraalta, donde los desarrolladores saben exactamente cuánta memoria tiene la aplicación, o incluso logran una situación de (casi) total ausencia de basura. De lo contrario, no se recomienda Epsilon GC.

El argumento de JVM para usar el recolector de basura Epsilon es: -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC.

Shenandoah

Shenandoah es un nuevo GC lanzado como parte de JDK 12. La ventaja clave de Shenandoah sobre G1 es que la mayor parte del ciclo de recolección de basura se realiza simultáneamente con los subprocesos de la aplicación. G1 solo puede evacuar áreas de Heap cuando la aplicación está suspendida, mientras que Shenandoah mueve objetos al mismo tiempo que la aplicación.

Shenandoah puede compactar objetos vivos, limpiar basura y liberar RAM casi tan pronto como se encuentra memoria libre. Dado que todo esto sucede al mismo tiempo, sin suspender la aplicación, Shenandoah consume más CPU.

Argumento de JVM para el recolector de basura de Shenandoah: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC.

ZGC

ZGC es otro GC lanzado como parte de JDK 11 y mejorado en JDK 12. Está diseñado para aplicaciones que requieren baja latencia (pausas de menos de 10 ms) y/o usan un Heap muy grande (varios terabytes).

Los principales objetivos de ZGC son baja latencia, escalabilidad y facilidad de uso. Para hacer esto, ZGC permite que la aplicación Java continúe ejecutándose mientras todas las operaciones de recolección de basura están en progreso. De forma predeterminada, ZGC libera la memoria no utilizada y la devuelve al sistema operativo.

Por lo tanto, el ZGC brinda una mejora significativa con respecto a otros GCS tradicionales al proporcionar tiempos muertos extremadamente bajos (generalmente dentro de los 2 ms).


El argumento de JVM para usar el recolector de basura ZGC es: - XX:+UnlockExperimentalVMOptions -XX:+UseZGC.

Nota: tanto Shenandoah como ZGC están programados para salir de la etapa experimental y entrar en producción con el lanzamiento de JDK 15.

Cómo elegir el recolector de basura adecuado

Si su aplicación no tiene requisitos estrictos de latencia, simplemente debe ejecutar la aplicación y dejar que la propia JVM elija el recopilador correcto.

En la mayoría de los casos, la configuración predeterminada funciona bien. Opcionalmente, puede ajustar el tamaño del almacenamiento dinámico para mejorar el rendimiento. Si el rendimiento aún no es el esperado, intente modificar el recopilador para adaptarlo a los requisitos de su aplicación.

  • de serie Si la aplicación tiene un conjunto de datos pequeño (hasta aproximadamente 100 MB) y/o se ejecutará en un solo procesador sin requisitos de latencia.
  • Paralelo _ Si la prioridad es el máximo rendimiento de la aplicación y no hay requisitos de latencia (o se aceptan pausas de un segundo o más).
  • CMS/G1 . Si el tiempo de respuesta es más importante que el rendimiento general, las pausas de recolección de basura deben ser más cortas que un segundo.
  • ZGC . Si el tiempo de respuesta es de alta prioridad y/o se trata de un Heap muy grande.

Beneficios de la recolección de basura

La recolección de basura de Java tiene muchas ventajas.

En primer lugar, simplifica el código. No hay necesidad de preocuparse por la asignación de memoria adecuada y los ciclos de liberación. Simplemente deja de usar el objeto en el código y la memoria que ocupaba se recuperará automáticamente en algún momento.

Los programadores que trabajan en lenguajes sin recolección de basura (como C y C++) tienen que implementar la gestión de memoria manual en su código.

La eficiencia de la memoria de Java también mejora porque el recolector de elementos no utilizados elimina los objetos sin referencia de la memoria del Heap. Esto libera la memoria del Heap para acomodar nuevos objetos.

Algunos programadores abogan por la gestión manual de la memoria en lugar de la recolección de basura, pero la recolección de basura ya es una función estándar en muchos lenguajes de programación populares.

Para escenarios donde el recolector de elementos no utilizados tiene un impacto negativo en el rendimiento, Java ofrece muchas opciones de ajuste que mejoran la eficiencia del GC.

Pautas para la recolección de basura

Evite los disparadores manuales

Además de los mecanismos básicos de recolección de basura, uno de los puntos más importantes de este proceso en Java es que no es determinista. Es decir, es imposible predecir cuándo ocurrirá exactamente en tiempo de ejecución.

Usando los métodos System.gc()o , Runtime.gc()puede incluir una sugerencia en su código para iniciar el recolector de basura, pero esto no garantiza que realmente se ejecute.

Usar herramientas de análisis

Si no tiene suficiente memoria para ejecutar su aplicación, experimentará ralentizaciones, tiempos prolongados de recolección de basura, eventos de "parada mundial" y, finalmente, errores de falta de memoria. Esto puede indicar que el Heap es demasiado pequeño, pero también puede indicar que la aplicación tiene una pérdida de memoria.

Puede usar una herramienta de monitoreo como jstat o Java Flight Recorder para ver si el uso del Heap crece indefinidamente, lo que podría indicar un error en el código.

Preferir la configuración predeterminada

Si tiene una aplicación Java pequeña e independiente, probablemente no necesite configurar la recolección de elementos no utilizados. La configuración predeterminada le servirá bien.

Use banderas JVM para personalizar

El mejor enfoque para configurar la recolección de basura en Java es establecer indicadores JVM. Los indicadores se pueden usar para establecer el recolector de basura (por ejemplo, Serial, G1, etc.), el tamaño inicial y máximo del Heap, el tamaño de las particiones del Heap (por ejemplo, Generación joven, Generación anterior) y mucho más .

Elige el grifo adecuado

Una buena pauta en términos de configuración inicial es la naturaleza de la aplicación personalizada. Por ejemplo, el recolector de basura simultáneo es eficiente, pero a menudo genera eventos de "parada mundial", lo que lo hace más adecuado para el procesamiento interno donde las pausas largas son aceptables.

Por otro lado, el recolector de basura CMS está diseñado para minimizar la latencia, lo que lo hace ideal para aplicaciones web donde la capacidad de respuesta es importante.

Conclusión

En este artículo, discutimos la recolección de elementos no utilizados de Java, cómo funciona y sus tipos.

Para muchas aplicaciones Java simples, no es necesario que el programador controle conscientemente la recolección de basura. Sin embargo, para aquellos que quieran desarrollar habilidades en Java, es importante comprender cómo funciona este proceso.

Esta es también una pregunta de entrevista muy popular para puestos intermedios y superiores en el desarrollo de back-end.

Fuente:

https://elsolitario.org/post/recoleccion-de-basura-en-java-que-es-y-como-funciona-en-la-jvm/

Report Page