Comprensión profunda de la máquina virtual de Java (segunda edición) Aprendizaje 2: Algoritmos de recolección de basura

Juicio de supervivencia del objeto

La máquina virtual necesita determinar cuáles de estos objetos están "vivos" antes de recuperar el montón.

Conteo de referencia

Principio: Agregar un contador de referencia al objeto. Siempre que haya un lugar para referirse a él, el valor del contador se incrementa en uno; cuando la referencia no es válida, el valor del contador se disminuye en uno; el objeto cuyo contador es 0 en cualquier el tiempo es imposible de volver a utilizar. .

Ventajas: implementación simple y alta eficiencia de juicio.

Desventaja: Es difícil resolver el problema de las referencias circulares entre objetos.

JVM no utiliza este método de juicio, pero es muy utilizado en lenguajes como FlashPlayer y Python.

Análisis de accesibilidad

En las principales implementaciones de los principales lenguajes de programación comerciales (Java, C#), el análisis de accesibilidad se utiliza para determinar si un objeto está vivo.

Principio: Mediante una serie de **Referencias** denominadas “GC Root” como punto de partida, la búsqueda parte de estos nodos, y el camino recorrido por la búsqueda se denomina Cadena de Referencia (Reference Chain). conectado a la Raíz de GC por cualquier cadena de referencia (es decir, el objeto es inalcanzable desde la Raíz de GC), prueba que el objeto no está disponible.

Nota: La Raíz GC se llama el objeto aquí en el libro.Después de leer la respuesta de R en Zhihu, creo que la referencia es más adecuada. Las referencias son variables almacenadas en la pila, los objetos son datos almacenados en el montón y generalmente operamos objetos a través de referencias (es decir, variables de referencia).

Entendamos, asumiendo que la recolección de basura es un río, y los objetos en el montón son botes en el río, entonces GC Roots son las anclas en el río. El punto de anclaje definitivamente no será arrastrado por el río, y los barcos que están conectados al punto de anclaje (es decir, los objetos alcanzables) tampoco serán arrastrados; y los barcos que están desconectados del punto de anclaje (que es decir, los objetos inalcanzables) no serán arrastrados), serán arrastrados lentamente por el río (es decir, reciclados por la basura).

Por favor agregue la descripción de la imagen
Como se muestra en la figura anterior, aunque object5, object6 y object7 están relacionados entre sí, GC Roots no puede acceder a ellos, por lo que se considerarán objetos reciclables.

¿Qué objetos se pueden usar como GC Roots?
En el lenguaje Java, los objetos que se pueden usar como GC Roots incluyen lo siguiente:

  1. Objetos a los que se hace referencia en la pila de la máquina virtual (tabla de variables locales en el marco de la pila);
  2. El objeto al que hace referencia la propiedad estática de la clase en el área del método;
  3. El objeto al que hace referencia la constante en el área del método;
  4. El objeto al que hace referencia JNI (es decir, el método nativo en general) en la pila de métodos nativos.

Hablando de recolección de basura, GC Root
no recolecta la generación anterior en GC joven, por lo que todos los objetos en la generación anterior se considerarán vivos en este momento, por lo que las referencias a la generación joven de estos objetos deben considerarse vivas. Por lo tanto, estas referencias se consideran parte del conjunto de raíces, por lo que hay más raíces de GC para GC jóvenes que para GC completo.

vive o muere

Sin embargo, incluso los objetos inalcanzables en el algoritmo de análisis de accesibilidad no son "deben morir". En este momento, se encuentran temporalmente en la etapa de "prueba". Para declarar realmente un objeto muerto, se requieren al menos dos procesos de marcado.

Por favor agregue la descripción de la imagen

primera marca

Si se descubre que el objeto no tiene una cadena de referencia conectada por GC Roots después del análisis de accesibilidad, se marcará por primera vez y se filtrará una vez. La condición de filtrado es si es necesario que el objeto ejecute el método finalize(). Cuando el objeto no anula el método finalize() , o la máquina virtual ha llamado al método finalize() , la máquina virtual trata ambos casos como "innecesarios para ejecutar".

segunda marca

Si se determina que el objeto es necesario para ejecutar el método finalize(), entonces el objeto se colocará en una cola llamada F-Queue, y luego por un subproceso Finalizer de baja prioridad creado automáticamente por la máquina virtual. La llamada "ejecución" aquí significa que la máquina virtual activa el método, pero no promete esperar a que se ejecuten hasta el final . La razón es que si un objeto es lento en el método finalize(), o un objeto infinito ocurre un bucle (casos más extremos), probablemente hará que otros objetos en la cola F-Queue esperen para siempre, e incluso que todo el sistema de recuperación de memoria se bloquee.

El método finalize() es la última oportunidad para que el objeto escape al destino de la muerte. Más tarde, el GC marcará el objeto en F-Queue para una segunda escala pequeña. Si el objeto quiere salvarse con éxito en finalize() - simplemente vuelva a conectarse con Puede establecer una asociación con cualquier objeto en la cadena de referencia, como asignarse a sí mismo a una variable de clase o una variable miembro de un objeto, se eliminará de la colección "para ser reciclado" en la segunda marca; si el objeto todavía no tiene escapatoria, entonces básicamente es realmente reciclado.

En la siguiente GC, los objetos escapados solo se recuperarán marcando una vez más.

Desventajas del método finalizar()

No se recomienda que todo el mundo utilice el método finalize(). El libro lo describe como " caro de ejecutar, incierto y no puede garantizar el orden de llamada de cada objeto ".
De hecho, el factor más intuitivo es que puede conducir fácilmente a OOM (desbordamiento de memoria) si se usa incorrectamente, de la siguiente manera:

/**
 * @author 小关同学
 * @create 2021/10/4
 */
public class FinalizeTest {
    
    

    public static class Test{
    
    
        private byte[] content = new byte[1024*1024];

        protected void finalize(){
    
    
            System.out.println("finalize方法被执行");
        }
    }

    public static void main(String[] args) throws Exception{
    
    
        for (int i = 0;i<1000;i++){
    
    
            Test test = new Test();
        }
    }
}
# 虚拟机参数设置:
# 最大堆内存
-Xmx5m
# 最小堆内存
-Xms5m

Resultado de la ejecución:
inserte la descripción de la imagen aquí
como se muestra en la figura anterior, se produjo un desbordamiento de memoria. El motivo también se mencionó anteriormente. El segundo proceso de marcado lo ejecuta un subproceso Finalizer de baja prioridad. Cuando el subproceso principal está creando frenéticamente nuevos objetos de prueba para ocupar espacio, el subproceso Finalizer también se ejecuta lentamente, lo que da como resultado que la velocidad de limpieza del espacio de memoria no pueda seguir el ritmo del espacio de memoria que se está ocupando, y se produce un desbordamiento de memoria.

Algoritmos de recolección de basura

Marcar-barrer

El algoritmo de "barrido de marcas" (Mark-Sweep) es el algoritmo de recopilación más básico, y los algoritmos de recopilación subsiguientes se mejoran en función de él. Para los objetos que deben reciclarse, todos los objetos marcados se reciclan uniformemente una vez que se completa el marcado ( es decir, las dos marcas anteriores).

Por favor agregue la descripción de la imagen
La imagen de arriba es de https://www.jianshu.com/p/74727c856da4

defecto:

  1. Cuestiones de eficiencia, tanto los procesos de marcado como los de limpieza son poco eficientes;
  2. Problema de espacio, después de borrar la marca, se generará una gran cantidad de fragmentos de memoria discontinuos. Demasiados fragmentos de espacio harán que cuando se deban asignar objetos más grandes en el futuro, no se pueda encontrar suficiente memoria continua y se tenga que realizar otra acción de recolección de elementos no utilizados. para activarse con antelación.

Algoritmo de copia (Copiando)

El algoritmo de "Copia" divide la memoria disponible en dos bloques de igual tamaño por capacidad, y solo usa uno de ellos a la vez. Cuando esta parte de la memoria se agota, los objetos supervivientes se copian en otra parte y luego el espacio de memoria usado se limpia de una vez, lo que resuelve el problema de la fragmentación de la memoria. Cuanto mayor sea la tasa de supervivencia del objeto, menor será la eficiencia .

Las máquinas virtuales comerciales actuales usan un algoritmo de replicación para recuperar la nueva generación Dividen la memoria en un espacio Eden más grande y dos espacios Survivor más pequeños, y usan Eden y uno de los espacios Survivor cada vez. Al reciclar, copie los objetos sobrevivientes en Eden y Survivor a otro espacio de Survivor a la vez, y finalmente limpie Eden y el espacio de Survivor que se acaba de usar.

HotSpot JVM divide la generación joven en tres partes: 1 área Eden y 2 áreas Survivor (llamadas From y To respectivamente), la proporción predeterminada es 8: 1: 1, y la proporción de tamaño predeterminada de Eden y From Survivor es 8: 1 , Es decir, el espacio de memoria disponible en cada nueva generación es el 90 % (80 %+10 %) de toda la capacidad de la nueva generación, y solo se "desperdiciará" el 10 % de la memoria (área To Survivor).

Nota: Cuando el espacio de Survivor no es suficiente, debe confiar en otra memoria (aquí se refiere a la generación anterior) para la garantía de asignación (Promoción de manejo).
Por favor agregue la descripción de la imagen

Descripción del proceso de recogida:

  1. Al comienzo de GC, los objetos solo existen en el área Eden y From Survivor area, To Survivor area está vacía.

  2. Inmediatamente después de GC, todos los objetos sobrevivientes en el área de Eden se copiarán en el área de To Survivor.

  3. En el área de From Survivor, los objetos sobrevivientes decidirán a dónde ir de acuerdo con su valor de edad (cada vez que el objeto sobreviva a un GC menor en el área de From Survivor, la edad aumentará en 1 año). Los objetos cuya edad alcance un determinado valor (el umbral de edad, que se -XX:MaxTenuringThresholdpuede establecer con ) se moverán a la antigüedad y los objetos que no alcancen el umbral se copiarán en el área Para el superviviente.

  4. Después de este GC, el área de Eden y el área de From Survivor se han despejado. En este momento, From Survivor y To Survivor intercambiarán sus roles, es decir, el nuevo To Survivor es From Survivor antes del último GC, y el nuevo From Survivor es To Survivor antes del último GC. En cualquier caso, se garantizará que el área denominada To Survivor esté vacía, es decir, siempre habrá un Espacio Survivor que esté vacío . Minor GC repetirá este proceso hasta que se llene el área Para el sobreviviente (es decir, cuando se copia en el área Para el sobreviviente, el área Para el sobreviviente no se puede cargar toda a la vez), después de que se llene el área Para el sobreviviente, todos los objetos se ser trasladado directamente a la vieja generación .

Mark-Compact

El algoritmo "Mark-Compact" es un algoritmo para reciclar la vejez. Su proceso de marcado es el mismo que el algoritmo "mark-clean", pero los pasos posteriores no son para limpiar los objetos reciclables directamente, sino para dejar que todos los Los objetos supervivientes se limpian Los objetos se mueven a un extremo y luego se limpia la memoria fuera del límite final.

inserte la descripción de la imagen aquí

La imagen de arriba es de: https://blog.51cto.com/u_4837471/2324575

Colección Generacional

El algoritmo de "Recopilación generacional" divide la memoria en varios bloques de acuerdo con el ciclo de vida de los objetos. Generalmente, el montón de Java se divide en la nueva generación y la generación anterior, de modo que se pueda adoptar el algoritmo de recopilación más apropiado de acuerdo con el características de la generación individual.

En la nueva generación, una gran cantidad de objetos mueren durante cada recolección de basura y solo unos pocos sobreviven, por lo que se usa el algoritmo de replicación y la recolección se puede completar con solo un pequeño costo de copiar los objetos sobrevivientes;
en la generación anterior , debido a que la tasa de supervivencia del objeto es demasiado alta, debe recuperarse utilizando un algoritmo de "marcar-barrer" o "marcar para limpiar" sin el espacio adicional garantizado para asignarlo.

Principio de implementación del algoritmo

enumerar nodos raíz

Enumerar los nodos raíz es encontrar GC Roots.Como dijimos anteriormente, en la máquina virtual HotSpot, el análisis de accesibilidad se usa para determinar si el objeto necesita ser reciclado. El análisis de accesibilidad necesita encontrar la "fuente", es decir, el nodo raíz (GC Roots).

pregunta

  1. consume mucho tiempo.
    Del análisis de accesibilidad anterior, sabemos que las raíces de GC se encuentran principalmente en referencias globales (atributos constantes o estáticos) y contextos de ejecución (tabla de variables locales en el marco de la pila);
    para verificar las referencias una por una en estas grandes cantidades de datos, es necesario consumirá mucho tiempo;
  2. Puestos de GC.
    Durante el análisis de accesibilidad, es necesario garantizar la coherencia de todo el sistema de ejecución, y la relación de referencia de los objetos no puede cambiar;
    todos los subprocesos de ejecución de Java deben pausarse durante GC (llamado "Stop The World", STW);
    (casi sin pausa ocurre En el recopilador CMS, la enumeración del nodo raíz también debe pausarse)
    La JVM inicia y completa automáticamente STW en segundo plano; cuando el usuario es invisible, todos los subprocesos de trabajo normales del usuario se detienen;

Resumen: para reducir el tiempo dedicado al análisis de accesibilidad, acorte el tiempo de SWT tanto como sea posible, reduciendo así el tiempo de inactividad de la máquina virtual.

Solución

Para reducir el tiempo dedicado al análisis de accesibilidad, la máquina virtual Hotspot utiliza un conjunto de estructuras de datos llamado OopMap, de modo que la máquina virtual puede saber directamente dónde se almacenan las referencias de objetos sin atravesar directamente la cadena de referencia completa cada vez, así que ejecute Una vez que el sistema está inactivo, no es necesario verificar todas, una por una, las ubicaciones de referencia en la localidad completa y el contexto de ejecución.

En Hotspot, la información de tipo de un objeto tiene su propio OopMap, que registra qué tipo de datos se encuentran en qué desplazamiento en el tipo de objeto. Entonces, el escaneo desde el comienzo del objeto hacia afuera puede ser preciso, y estos datos se calculan durante el proceso de carga de clases.

El papel de OopMap:

  1. Evite escaneos de pila completa y acelere la enumeración de nodos raíz
  2. Ayuda a Hotspot a lograr una GC precisa

GC conservador

Conservative GC es GC que no reconoce punteros (referencias) y no punteros

defecto:

  1. No identificar referencias y no referencias conducirá a algunos objetos que deberían haberse reclamado, pero hay punteros sospechosos que los señalan, lo que hace que escapen a la recopilación de GC;
  2. Dado que no se sabe si los punteros sospechosos son punteros reales, sus valores no se pueden reescribir; mover objetos significa modificar punteros, por lo que solo se pueden usar algoritmos de GC que no mueven objetos, como: Mark-Sweep

GC semiconservador

La GC semiconservadora no registra información de referencia en la pila, pero registra información de referencia en el objeto

defecto:

  1. En tiempo de ejecución, debe tener suficientes metadatos en el objeto

GC exacto

La máquina virtual necesita saber qué tipo de datos hay en todas las ubicaciones de la memoria y si apunta a una referencia en el montón de GC.

Varios métodos precisos de implementación de GC:

  1. Hacer la etiqueta de los datos en sí (común en GC semiconservadora);
  2. Deje que el compilador genere códigos de escaneo especiales para cada método;
  3. Registre la información de tipo desde el exterior y guárdela como una tabla de mapeo, como: OopMap en Hotspot.

Punto seguro

Con OopMap, HotSpot puede completar de forma rápida y precisa la enumeración de GC Roots. Pero surge otra pregunta, ¿dónde vamos a crear el OopMap?
Durante la ejecución del programa, los cambios de la referencia ocurren constantemente, si cada instrucción genera un OopMap, ocupará demasiado espacio, por lo que hay un punto seguro (Safe Point).
Solo realice pausas del GC en puntos seguros, siempre que el registro de cambios de referencia se complete antes de la pausa del GC.

Cómo elegir un punto seguro

La selección del punto de seguridad se basa básicamente en si el programa tiene las características de permitir que el programa se ejecute durante mucho tiempo, giro, salto anormal, etc., por lo que las instrucciones con estas funciones generarán Punto Seguro.

Cómo los puntos seguros pueden interrumpir subprocesos

Hay dos opciones a elegir:

  1. Suspensión preventiva
    Cuando ocurre GC, todos los subprocesos se interrumpen primero. Si se encuentra que el lugar donde se interrumpe el subproceso no está en el punto seguro, el subproceso se restaura y se "ejecuta" hasta el punto seguro.
  2. Interrupción activa (suspensión voluntaria)
    Cuando el GC necesita interrumpir el subproceso, no opera directamente en el subproceso, sino que simplemente establece un indicador. Cuando cada subproceso se ejecuta, sondea activamente el indicador y cuando encuentra que el indicador de interrupción es cierto, se interrumpe y se suspende. . El lugar donde la bandera de votación coincide con el punto seguro, más el lugar donde la creación del objeto necesita asignar memoria.

HotSpot utiliza interrupciones proactivas.

Región segura

El punto de seguridad asegura que cuando se ejecuta el programa, encontrará un punto de seguridad que puede ingresar al GC en poco tiempo, pero si el subproceso no asigna tiempo de CPU, el subproceso debe estar en el estado de Suspensión o Bloqueado, y no puede responder La solicitud de interrupción de la JVM va a un punto seguro para suspender. Safe Region resuelve este problema.

Un área segura significa que en un fragmento de código, la relación de referencia no cambiará. Es seguro iniciar un GC en cualquier lugar de esta área.

Cuando el código se ejecuta en el área segura, primero identifica que ha ingresado al área segura, por lo que si la JVM inicia GC durante este período, no necesita preocuparse por los subprocesos que se identifican en el área segura. sale del área segura, comprobará si el sistema ha completado la enumeración del nodo raíz (o todo el proceso de GC), si es así, el subproceso continúa ejecutándose, de lo contrario, debe esperar hasta que reciba una señal de que es seguro salir del área segura. Area segura.

PD: También puedes ir a mi blog personal para ver más contenido
Dirección del blog personal: blog de un compañero de clase de Xiaoguan

Supongo que te gusta

Origin blog.csdn.net/weixin_45784666/article/details/120601240
Recomendado
Clasificación