El mecanismo de recolección de basura de la plataforma Java ha mejorado significativamente la eficiencia del desarrollador, pero un recolector de basura mal implementado puede consumir demasiados recursos de la aplicación. En la tercera parte de la serie de optimización del rendimiento de la máquina virtual Java, Eva Andreasson presenta a los principiantes de Java el modelo de memoria y el mecanismo de recolección de basura de la plataforma Java. Ella explica por qué la fragmentación (no la recolección de basura) es el principal problema en el rendimiento de las aplicaciones Java, y por qué la recolección y compactación de basura generacional son actualmente las formas principales (pero no las más innovadoras) de lidiar con la fragmentación de las aplicaciones Java.
El propósito de la recolección de basura (GC) es liberar la memoria ocupada por los objetos Java a los que ya no hace referencia ningún objeto activo. Es la parte central del mecanismo de gestión de memoria dinámica de la máquina virtual Java. Durante un ciclo típico de recolección de basura, todos los objetos a los que todavía se hace referencia (y, por lo tanto, a los que se puede acceder) se retienen, mientras que aquellos a los que ya no se hace referencia se liberan y el espacio que ocupan se recupera para su asignación a nuevos objetos.
Para comprender el mecanismo de recolección de basura y varios algoritmos de recolección de basura, primero necesita saber algo sobre el modelo de memoria de la plataforma Java.
Recolección de basura y modelo de memoria de la plataforma Java.
Cuando inicia un programa Java desde la línea de comando y especifica el parámetro de inicio -Xmx (por ejemplo: java -Xmx:2g MyApp), la memoria del tamaño especificado se asigna al proceso Java, que es el llamado montón de Java. . Este espacio de direcciones de memoria dedicada se utiliza para almacenar objetos creados por programas Java (y, a veces, la JVM). A medida que la aplicación se ejecuta y asigna memoria continuamente para nuevos objetos, el montón de Java (es decir, el espacio de direcciones de memoria dedicado) se llenará lentamente.
Con el tiempo, el montón de Java se llenará, lo que significa que el subproceso de asignación de memoria no puede encontrar un espacio contiguo lo suficientemente grande para asignar memoria para el nuevo objeto. En este momento, la JVM decide notificar al recolector de basura e iniciar la recolección de basura. La recolección de basura también se puede activar llamando a System.gc() en el programa, pero usar System.gc() no garantiza que se realizará la recolección de basura. Antes de cualquier recolección de basura, el mecanismo de recolección de basura primero determinará si es seguro realizar la recolección de basura. Cuando todos los subprocesos activos de la aplicación estén en un punto seguro, se puede iniciar una recolección de basura. Por ejemplo, la recolección de basura no se puede realizar cuando se asigna memoria para un objeto, o la recolección de basura no se puede realizar mientras se optimizan las instrucciones de la CPU, porque es probable que se pierda el contexto y el resultado final sea incorrecto.
El recolector de basura no puede recuperar ningún objeto con referencias activas, lo que rompería la especificación de la máquina virtual Java. No es necesario reciclar los objetos muertos inmediatamente, porque los objetos muertos eventualmente serán reciclados mediante la recolección de basura posterior. Aunque hay muchas formas de implementar la recolección de basura, los dos puntos anteriores son los mismos para todas las implementaciones de recolección de basura. El verdadero desafío de la recolección de basura es cómo identificar si un objeto está vivo y cómo recuperar memoria sin afectar la aplicación tanto como sea posible. Por lo tanto, el recolector de basura tiene los dos objetivos siguientes:
1. Libere rápidamente memoria sin referencia para satisfacer las necesidades de asignación de memoria de la aplicación y evitar el desbordamiento de la memoria.
2. Minimizar el impacto en el rendimiento de la aplicación en ejecución (latencia y rendimiento) al recuperar memoria.
Dos tipos de recolección de basura.
En el primer artículo de esta serie, presenté dos métodos de recolección de basura, a saber, el recuento de referencias y la recolección de seguimiento. A continuación, exploramos más a fondo estos dos enfoques e introducimos algunos algoritmos de recopilación de seguimiento utilizados en entornos de producción.
Recopilador de recuento de referencias
El recopilador de recuento de referencias registra el número de referencias que apuntan a cada objeto Java. Una vez que el número de referencias que apuntan a un objeto llega a 0, el objeto se puede reciclar inmediatamente. Esta inmediatez es la principal ventaja de un recopilador con recuento de referencias, y casi no hay gastos generales en el mantenimiento de la memoria a la que no apunta ninguna referencia, pero realizar un seguimiento del último recuento de referencias para cada objeto es costoso.
La principal dificultad del recopilador de recuento de referencias es cómo garantizar la precisión del recuento de referencias. Otra dificultad bien conocida es cómo tratar las referencias circulares. Si dos objetos hacen referencia entre sí y otros objetos activos no hacen referencia a ellos, entonces la memoria de los dos objetos nunca se recuperará porque el número de referencias que apuntan a ninguno de los objetos es 0. El reciclaje de memoria de estructuras de referencia circulares requiere un análisis importante (Nota del traductor: Análisis global en el montón de Java), lo que aumentará la complejidad del algoritmo y, por lo tanto, generará una sobrecarga adicional para la aplicación.
recolector de rastros
El recopilador de seguimiento se basa en el supuesto de que todos los objetos vivos se pueden encontrar iterando referencias (referencias y referencias de referencias) a un conjunto inicial conocido de objetos vivos. El conjunto inicial de objetos activos (también llamados objetos raíz) se puede determinar analizando registros, objetos globales y marcos de pila. Después de determinar el conjunto inicial de objetos, el recopilador de seguimiento sigue las relaciones de referencia de estos objetos y marca los objetos señalados por las referencias como objetos activos en secuencia, de modo que el conjunto de objetos activos conocidos continúa expandiéndose. Este proceso continúa hasta que todos los objetos a los que se hace referencia se marcan como objetos activos y se recupera la memoria de aquellos objetos que no han sido marcados.
El recopilador de seguimiento se diferencia del recopilador de recuento de referencias principalmente en que puede manejar estructuras de referencia circulares. La mayoría de los recolectores de rastreo descubren objetos sin referencia en estructuras de referencia circulares durante la fase de marcado.
El recopilador de seguimiento es el método de administración de memoria más utilizado en lenguajes dinámicos y actualmente es el método más común en Java. También se ha verificado en entornos de producción durante muchos años. A continuación, presentaré el recopilador de seguimiento comenzando con algunos algoritmos para implementar la recopilación de seguimiento.
Algoritmo de recopilación de seguimiento
Los recolectores de basura de copia y barrido de marcas no son nada nuevo, pero siguen siendo los dos algoritmos más comunes para implementar recolecciones de seguimiento en la actualidad.
Copiando el recolector de basura
El recolector de basura de copia tradicional utiliza dos espacios de direcciones en el montón (es decir, desde el espacio y hacia el espacio). Cuando se realiza la recolección de basura, los objetos activos en el espacio desde se copian al espacio desde. se eliminan (Nota del traductor: después de copiar al espacio to o a la generación anterior), todo el espacio from se puede reciclar. Cuando el espacio se asigna nuevamente, el espacio to se usará primero (Nota del traductor: es decir, el espacio to). de la ronda anterior se utilizará como la nueva ronda de espacio desde el espacio).
En la implementación inicial de este algoritmo, el espacio desde y hacia el espacio cambiaban continuamente sus posiciones, es decir, cuando el espacio hacia está lleno y se activa la recolección de basura, el espacio hacia el espacio se convierte en el espacio desde, como se muestra en la Figura 1. .
Figura 1 Secuencia tradicional de recolección de basura de copia
El último algoritmo de copia permite utilizar cualquier espacio de direcciones del montón como hacia el espacio y desde el espacio. De esta manera no necesitan intercambiar posiciones entre sí, sino que cambian de posición de forma lógica.
La ventaja del recopilador de copias es que los objetos copiados en el espacio están organizados de forma compacta y no hay fragmentación alguna. La fragmentación es un problema común que enfrentan otros recolectores de basura y también es el tema principal que discutiré más adelante.
Desventajas del coleccionista de copias.
En términos generales, el recolector de copias detiene el mundo, lo que significa que mientras la recolección de basura esté en progreso, la aplicación no se puede ejecutar. Con esta implementación, cuantas más cosas necesite copiar, mayor será el impacto en el rendimiento de la aplicación. Esta es una desventaja para las aplicaciones que dependen del tiempo de respuesta. Al utilizar el recolector de copias, también debe considerar el peor escenario (es decir, todos los objetos en el espacio desde son objetos activos). En este momento, debe preparar un espacio lo suficientemente grande para mover estos objetos activos, por lo que debe prepararse. El espacio debe ser lo suficientemente grande para instalar todos los objetos en el espacio. Debido a esta limitación, la utilización de la memoria del algoritmo de copia es ligeramente insuficiente (Nota del traductor: en el peor de los casos, el espacio hacia el espacio debe tener el mismo tamaño que el espacio desde, por lo que solo se utiliza el 50%).
coleccionista de marcas claras
La mayoría de las JVM comerciales implementadas en entornos de producción empresarial utilizan un recolector de marcas y barrido (o marcas) porque no replica el impacto de un recolector de basura en el rendimiento de la aplicación. Los coleccionistas de marcas más famosos incluyen CMS, G1, GenPar y DeterministicGC.
El recopilador de barrido de marcas rastrea las referencias de objetos y marca cada objeto encontrado como activo utilizando un bit de bandera. Esta bandera generalmente corresponde a una dirección o grupo de direcciones en el montón. Por ejemplo: el bit activo puede ser un bit en el encabezado del objeto (Nota del traductor: bit) o un vector de bits o un mapa de bits.
Una vez completado el marcado, se ingresa a la fase de limpieza. La fase de limpieza generalmente atraviesa el montón nuevamente (no solo los objetos marcados como activos, sino todo el montón) para ubicar espacios de direcciones de memoria contiguos no marcados (la memoria no marcada es libre y reciclable), y luego el recopilador los organiza en listas libres. El recolector de basura puede tener varias listas libres (generalmente divididas según el tamaño del bloque de memoria). Algunos recolectores de JVM (por ejemplo: JRockit Real Time) incluso dividen dinámicamente la lista libre según el análisis del rendimiento de la aplicación y las estadísticas del tamaño del objeto.
Después de la fase de limpieza, la aplicación puede volver a asignar memoria. Al asignar memoria para un nuevo objeto de la lista libre, el bloque de memoria recién asignado debe ajustarse al tamaño del nuevo objeto, o al tamaño promedio de objeto del subproceso, o al tamaño TLAB de la aplicación. Encontrar bloques de memoria del tamaño adecuado para nuevos objetos ayuda a optimizar la memoria y reducir la fragmentación.
Marca: borrar defectos de coleccionista
El tiempo de ejecución de la fase de marca depende de la cantidad de objetos activos en el montón, mientras que el tiempo de ejecución de la fase de limpieza depende del tamaño del montón. Por lo tanto, para situaciones donde la configuración del montón es grande y hay muchos objetos activos en el montón, el algoritmo de barrido de marcas tendrá un cierto tiempo de pausa.
Para aplicaciones que consumen mucha memoria, puede ajustar los parámetros de recolección de basura para adaptarlos a diversos escenarios y necesidades de la aplicación. En muchos casos, este ajuste al menos pospone el riesgo que representa la fase de marca/barrido para la aplicación o el acuerdo de servicio SLA (SLA aquí se refiere al tiempo de respuesta que la aplicación debe lograr). Pero el ajuste solo es efectivo para cargas específicas y tasas de asignación de memoria. Los cambios de carga o las modificaciones en la aplicación en sí requieren un nuevo ajuste.
Implementación del recolector de barrido de marcas.
Existen al menos dos métodos comercialmente probados para implementar la recolección de basura con barrido de marcas. Una es la recolección de basura paralela y la otra es la recolección de basura simultánea (o la mayoría de las veces simultánea).
Colector paralelo
La recolección paralela significa que los subprocesos de recolección de basura utilizan los recursos en paralelo. La mayoría de las implementaciones comerciales de recolección paralela son recolectores que detienen el mundo, en los que todos los subprocesos de la aplicación se pausan hasta que se completa una recolección de basura. Debido a que los recolectores de basura pueden usar los recursos de manera eficiente, generalmente obtienen mejores puntajes en los puntos de referencia de rendimiento, como. ESPEC.bb. Si el rendimiento es fundamental para su aplicación, un recolector de basura paralelo es una buena opción.
El principal costo de la recolección paralela (especialmente para entornos de producción) es que los subprocesos de la aplicación no pueden funcionar correctamente durante la recolección de basura, al igual que el recolector de copias. Por lo tanto, el uso de recopiladores paralelos tendrá un impacto significativo en las aplicaciones sensibles al tiempo de respuesta. Especialmente cuando hay muchas estructuras complejas de objetos activos en el espacio del montón, hay muchas referencias de objetos que deben rastrearse. (Recuerde que el tiempo que le toma al recolector de barrido de marcas recuperar memoria depende del tiempo que le toma rastrear la colección de objetos activos más el tiempo que le toma recorrer todo el montón). Con el enfoque paralelo, la aplicación se pausa durante el todo el tiempo de recolección de basura.
recopilador concurrente
Los recolectores de basura concurrentes son más adecuados para aplicaciones sensibles al tiempo de respuesta. La concurrencia significa que el subproceso de recolección de basura y el subproceso de la aplicación se ejecutan al mismo tiempo. El hilo de recolección de basura no posee todos los recursos, por lo que debe decidir cuándo iniciar una recolección de basura, dejando tiempo suficiente para rastrear la recolección de objetos activos y recuperar la memoria antes de que la memoria de la aplicación se desborde. Si la recolección de basura no se completa a tiempo, la aplicación generará un error de desbordamiento de memoria. Por otro lado, no desea que la recolección de basura demore demasiado porque consumirá los recursos de la aplicación y afectará el rendimiento. Mantener este equilibrio requiere habilidad, por lo que se utilizan heurísticas para determinar cuándo iniciar la recolección de basura y cuándo elegir optimizaciones de recolección de basura.
Otra dificultad es determinar cuándo es seguro realizar algunas operaciones (operaciones que requieren una instantánea del montón completa y precisa), como la necesidad de saber cuándo se completa la fase de marca para poder ingresar a la fase de limpieza. Esto no es un problema para un recolector paralelo que detiene el mundo, porque el mundo ya está en pausa (Nota del traductor: el hilo de la aplicación está en pausa y el hilo de recolección de basura monopoliza los recursos). Pero para los recolectores simultáneos, puede que no sea seguro cambiar inmediatamente de la fase de marcado a la fase de limpieza. Si un subproceso de aplicación modifica una parte de la memoria que ha sido rastreada y marcada por el recolector de basura, se pueden generar nuevas referencias sin marcar. En algunas implementaciones de colecciones concurrentes, esto puede hacer que la aplicación se quede atrapada en un bucle de anotaciones repetidas durante mucho tiempo sin poder obtener memoria libre cuando la aplicación la necesita.
De la discusión hasta ahora sabemos que existen muchos recolectores de basura y algoritmos de recolección de basura, cada uno adecuado para tipos de aplicaciones específicos y diferentes cargas. No solo algoritmos diferentes, sino implementaciones de algoritmos diferentes. Por lo tanto, es mejor comprender las necesidades de la aplicación y sus propias características antes de especificar un recolector de basura. A continuación, presentaremos algunos errores del modelo de memoria de la plataforma Java. Los errores aquí se refieren a algunas suposiciones que los programadores de Java son propensos a hacer en un entorno de producción que cambia dinámicamente y que empeoran el rendimiento de la aplicación.
Por qué el Tuning no puede reemplazar la recolección de basura
La mayoría de los programadores de Java saben que existen muchas opciones para optimizar programas Java. Varios parámetros opcionales de JVM, recolector de basura y ajuste del rendimiento permiten a los desarrolladores dedicar mucho tiempo a ajustar el rendimiento sin fin. Esto ha llevado a algunas personas a concluir que la recolección de basura es mala y que ajustar para que las recolecciones de basura ocurran con menos frecuencia o duren menos es una buena solución, pero esto es arriesgado.
Considere el ajuste para una aplicación específica. La mayoría de los parámetros de ajuste (como la tasa de asignación de memoria, el tamaño del objeto, el tiempo de respuesta) se basan en la tasa de asignación de memoria de la aplicación (Nota del traductor: u otros parámetros) según el volumen de datos de prueba actual. En última instancia, puede conducir a los dos resultados siguientes:
1. Un caso de uso que pasa las pruebas falla en producción.
2. Los cambios en el volumen de datos o los cambios en las aplicaciones requieren un nuevo ajuste.
El ajuste es iterativo y los recolectores de basura simultáneos en particular pueden requerir muchos ajustes (especialmente en un entorno de producción). Se necesitan heurísticas para satisfacer las necesidades de la aplicación. Para hacer frente al peor de los casos, el resultado del ajuste puede ser una configuración muy rígida, lo que también conduce a un gran desperdicio de recursos. Este enfoque de ajuste es una búsqueda quijotesca. De hecho, cuanto más optimice el recolector de basura para que coincida con una carga específica, más lejos estará de la naturaleza dinámica del tiempo de ejecución de Java. Después de todo, ¿cuántas aplicaciones tienen una carga estable y qué tan confiable se puede esperar que sea la carga?
Entonces, si no te concentras en el ajuste, ¿qué puedes hacer para evitar errores de falta de memoria y mejorar los tiempos de respuesta? Lo primero es encontrar los principales factores que afectan el rendimiento de las aplicaciones Java.
fragmentación
El factor que afecta el rendimiento de la aplicación Java no es el recolector de basura, sino la fragmentación y cómo el recolector de basura maneja la fragmentación. La llamada fragmentación es un estado en el que hay espacio libre en el espacio del montón, pero no hay un espacio de memoria contiguo lo suficientemente grande para asignar memoria para nuevos objetos. Como se mencionó en el primer artículo, la fragmentación de la memoria es un TLAB de espacio que queda en el montón o el espacio ocupado por objetos pequeños que se liberan entre objetos de larga vida.
Con el tiempo y a medida que se ejecuta la aplicación, esta fragmentación se extiende por todo el montón. En algunos casos, el uso de parámetros ajustados estáticamente puede ser peor porque no satisfacen las necesidades dinámicas de la aplicación. Las aplicaciones no pueden utilizar eficientemente este espacio fragmentado. Si no se hace nada, se producirán sucesivas recolecciones de basura en las que el recolector de basura intentará liberar memoria para asignarla a nuevos objetos. En el peor de los casos, incluso las sucesivas recolecciones de basura no pueden liberar más memoria (demasiada fragmentación), y luego la JVM tiene que generar un error de desbordamiento de memoria. Puede resolver la fragmentación reiniciando la aplicación para que el montón de Java tenga espacio de memoria contiguo para asignar nuevos objetos. Reiniciar el programa provoca un tiempo de inactividad y, después de un tiempo, el montón de Java volverá a llenarse de fragmentos, lo que obligará a otro reinicio.
Los errores de falta de memoria que bloquean el proceso y los registros que muestran que el recolector de basura está sobrecargado indican que la recolección de basura está intentando liberar memoria y que el montón está muy fragmentado. Algunos programadores intentarán resolver el problema de la fragmentación optimizando nuevamente el recolector de basura. Pero creo que deberíamos encontrar formas más innovadoras de resolver este problema. Las siguientes secciones se centrarán en dos soluciones a la fragmentación: recolección generacional de basura y compactación.
Recolección de basura generacional
Es posible que haya escuchado la teoría de que la mayoría de los objetos en un entorno de producción tienen una vida corta. La recolección de basura generacional es una estrategia de recolección de basura derivada de esta teoría. En la recolección de basura generacional, dividimos el montón en diferentes espacios (o generaciones), y cada espacio almacena objetos de diferentes edades. La llamada edad de un objeto es la cantidad de ciclos de recolección de basura que el objeto ha sobrevivido (es decir, qué edad tiene el objeto).
Cuando no queda espacio restante para asignar en la nueva generación, los objetos activos de la nueva generación se moverán a la generación anterior (generalmente solo hay dos generaciones. Nota del traductor: solo los objetos que cumplan una determinada edad se moverán a la generación anterior). Generación anterior) La recolección de basura a menudo usa un recolector de copias unidireccional. Algunas JVM más modernas usan recolectores paralelos en la nueva generación. Por supuesto, se pueden implementar diferentes algoritmos de recolección de basura para la nueva generación y la generación anterior. Si utiliza un recopilador paralelo o un recopilador de copias, su joven coleccionista será un coleccionista que detendrá el mundo (consulte la explicación anterior).
La generación anterior se asigna a objetos que se han eliminado de la nueva generación. Estos objetos han sido referenciados durante mucho tiempo o son referenciados por alguna colección de objetos de la nueva generación. Ocasionalmente, los objetos grandes se asignan directamente a la generación anterior porque el costo de mover objetos grandes es relativamente alto.
Tecnología generacional de recolección de basura.
En la recolección de basura generacional, la recolección de basura se ejecuta con menos frecuencia en la generación anterior y con mayor frecuencia en la nueva generación, y también esperamos que el ciclo de recolección de basura en la nueva generación sea más corto. En casos raros, la generación joven puede ser recolectada con más frecuencia que la generación anterior. Esto puede suceder si hace que la generación joven sea demasiado grande y la mayoría de los objetos en su aplicación vivan durante mucho tiempo. En este caso, si la generación anterior se configura demasiado pequeña para acomodar todos los objetos de larga duración, la recolección de basura de la generación anterior también tendrá dificultades para liberar espacio para los objetos que se trasladan. Sin embargo, en términos generales, la recolección de basura generacional puede permitir que las aplicaciones logren un mejor rendimiento.
Otro beneficio de dividir a la nueva generación es que resuelve en cierta medida el problema de la fragmentación o pospone el peor de los casos. Esos objetos pequeños con tiempos de supervivencia cortos pueden haber causado problemas de fragmentación, pero todos se limpian en la recolección de basura de nueva generación. Dado que a los objetos de larga vida se les asigna un espacio más compacto cuando se trasladan a la generación anterior, la generación anterior también es más compacta. Con el tiempo (si su aplicación se ejecuta durante suficiente tiempo), la generación anterior también se fragmentará, lo que requerirá la ejecución de una o más recolecciones de basura completas, y la JVM también puede generar errores de falta de memoria. Pero la creación de una nueva generación pospone el peor de los casos, que es suficiente para muchas aplicaciones. Para la mayoría de las aplicaciones, reduce la frecuencia de la recolección de basura que detiene el mundo y la posibilidad de errores de falta de memoria.
Optimizar la recolección de basura generacional
Como se mencionó anteriormente, el uso de la recolección de basura generacional implica un trabajo de ajuste repetido, como ajustar el tamaño de la generación joven, la tasa de promoción, etc. No puedo enfatizar la desventaja de un tiempo de ejecución de aplicación específico: elegir un tamaño fijo optimiza la aplicación, pero también reduce la capacidad del recolector de basura para hacer frente a los cambios dinámicos, que son inevitables.
El primer principio para la nueva generación es aumentarlo tanto como sea posible garantizando al mismo tiempo el tiempo de retraso durante la recolección de basura de detener el mundo y, al mismo tiempo, reservar suficiente espacio en el montón para los objetos que sobrevivan a largo plazo. Aquí hay algunos factores adicionales a considerar al ajustar un recolector de basura generacional:
1. La mayoría de la nueva generación son recolectores de basura que detienen el mundo. Cuanto mayor sea la configuración de la nueva generación, mayor será el tiempo de pausa correspondiente. Por lo tanto, para las aplicaciones que se ven muy afectadas por los tiempos de pausa de la recolección de basura, considere cuidadosamente qué tan grande es la generación joven.
2. Se pueden utilizar diferentes algoritmos de recolección de basura en diferentes generaciones. Por ejemplo, la recolección de basura paralela se usa en la generación joven y la recolección de basura concurrente se usa en la generación anterior.
3. Cuando se descubre que la promoción frecuente (Nota del traductor: pasar de la nueva generación a la vieja generación) falla, significa que hay demasiados fragmentos en la vieja generación, lo que significa que no hay suficiente espacio en la vieja generación. para almacenar objetos trasladados de la nueva generación. En este punto, puede ajustar la tasa de promoción (es decir, ajustar la edad de la promoción) o asegurarse de que el algoritmo de recolección de basura de la generación anterior esté realizando la compresión (que se analiza en el siguiente párrafo) y ajustar la compresión para adaptarse a la carga de la aplicación. . También es posible aumentar el tamaño del montón y el tamaño de cada generación, pero esto extenderá aún más el tiempo de pausa en la generación anterior. Sepa que la fragmentación es inevitable.
4. La recolección de basura generacional es la más adecuada para este tipo de aplicaciones. Tienen muchos objetos pequeños con tiempos de supervivencia cortos. Muchos objetos se reciclan en la primera ronda del ciclo de recolección de basura. Para tales aplicaciones, la recolección de basura generacional puede reducir efectivamente la fragmentación y retrasar el impacto de la fragmentación.
compresión
Aunque la recolección de basura generacional retrasa la aparición de errores de fragmentación y falta de memoria, la compresión es la única solución real al problema de la fragmentación. La compactación es una estrategia de recolección de basura que libera bloques contiguos de memoria moviendo objetos, liberando así suficiente espacio para crear nuevos objetos.
Mover objetos y actualizar referencias de objetos son operaciones para detener el mundo que generarán una cierta cantidad de consumo (con una excepción, que se analizará en el próximo artículo de esta serie). Cuantos más objetos sobrevivan, mayor será el tiempo de pausa provocado por la compactación. En situaciones donde queda poco espacio restante y una fragmentación severa (generalmente porque el programa ha estado ejecutándose durante mucho tiempo), puede haber una pausa de unos segundos al compactar áreas con muchos objetos vivos, y cuando se acerca el desbordamiento de la memoria, se puede comprimir el El montón completo puede tardar incluso decenas de segundos.
El tiempo de pausa para la compactación depende de la cantidad de memoria que se debe mover y la cantidad de referencias que se deben actualizar. El análisis estadístico muestra que cuanto mayor es el montón, mayor es el número de objetos activos que deben moverse y actualizarse las referencias. El tiempo de pausa es de aproximadamente 1 segundo por cada 1 GB a 2 GB de objetos vivos que se mueven, y para un montón de 4 GB es probable que haya un 25 % de objetos vivos, por lo que habrá pausas ocasionales de aproximadamente 1 segundo.
Muro de memoria de aplicaciones y compresión
El muro de memoria de la aplicación se refiere al tamaño del montón que se puede establecer antes de una pausa causada por la recolección de basura (por ejemplo: compactación). Dependiendo del sistema y la aplicación, la mayoría de los muros de memoria de las aplicaciones Java oscilan entre 4 GB y 20 GB. Esta es la razón por la que la mayoría de las aplicaciones empresariales se implementan en varias JVM más pequeñas en lugar de en unas pocas JVM más grandes. Consideremos esto: cuántos diseños e implementaciones de aplicaciones Java empresariales modernas están definidos por las limitaciones de compresión de la JVM. En este caso, para evitar el tiempo de pausa de la desfragmentación del montón, nos conformamos con una implementación de múltiples instancias que era más costosa de administrar. Esto es un poco extraño considerando las grandes capacidades de almacenamiento del hardware actual y la necesidad de mayor memoria para las aplicaciones Java de clase empresarial. Por qué solo se configuran unos pocos GB de memoria para cada instancia. La compresión concurrente derribará el muro de la memoria, que es el tema de mi próximo artículo.
Resumir
Este artículo es una introducción a la recolección de basura para ayudarlo a comprender los conceptos y mecanismos de la recolección de basura y, con suerte, motivarlo a leer más artículos relacionados. Muchas de las cosas discutidas aquí existen desde hace mucho tiempo y en el próximo artículo se presentarán algunos conceptos nuevos. Por ejemplo, la compresión concurrente la implementa actualmente Zing JVM de Azul. Es una tecnología emergente de recolección de basura que incluso intenta redefinir el modelo de memoria de Java, especialmente porque la memoria y la potencia de procesamiento continúan mejorando en la actualidad.
Aquí hay algunos puntos clave sobre la recolección de basura que he resumido:
1. Los diferentes algoritmos e implementaciones de recolección de basura se adaptan a las diferentes necesidades de las aplicaciones. El recolector de basura de seguimiento es el recolector de basura más utilizado en las máquinas virtuales Java comerciales.
2. La recolección de basura en paralelo utiliza todos los recursos en paralelo al realizar la recolección de basura. Por lo general, es un recolector de basura que detiene el mundo y, por lo tanto, tiene un mayor rendimiento, pero los subprocesos de trabajo de la aplicación deben esperar a que se complete el subproceso de recolección de basura, lo que tiene un cierto impacto en el tiempo de respuesta de la aplicación.
3. Recolección de basura simultánea: mientras se realiza la recolección, el subproceso de trabajo de la aplicación aún se está ejecutando. Un recolector de basura concurrente debe completar la recolección de basura antes de que la aplicación necesite memoria.
4. La recolección de basura generacional ayuda a retrasar la fragmentación, pero no puede eliminarla. La recolección de basura generacional divide el montón en dos espacios, un espacio para objetos nuevos y el otro para objetos antiguos. La recolección de basura generacional es adecuada para aplicaciones con muchos objetos pequeños que tienen una vida útil corta.
5. La compresión es la única forma de resolver la fragmentación. La mayoría de los recolectores de basura realizan la compresión en forma de detener el mundo. Cuanto más se ejecuta el programa, más complejas son las referencias a los objetos y más desigualmente distribuidos están los tamaños de los objetos, lo que conducirá a tiempos de compresión más largos. El tamaño del montón también afecta el tiempo de compactación, ya que puede haber más objetos activos y referencias que deban actualizarse.
6. El ajuste ayuda a retrasar los errores de desbordamiento de la memoria. Pero el resultado de un ajuste excesivo es una configuración rígida. Antes de comenzar a ajustar mediante un enfoque de prueba y error, asegúrese de comprender la carga en su entorno de producción, los tipos de objetos de su aplicación y las características de sus referencias de objetos. Es posible que las configuraciones demasiado rígidas no puedan soportar cargas dinámicas, así que asegúrese de comprender las consecuencias al establecer valores no dinámicos.
El siguiente artículo de esta serie es: Una discusión en profundidad sobre el algoritmo de recolección de basura C4 (Recolector de compactación continua concurrente), ¡así que estad atentos!
(Finaliza el texto completo)