Recolector de basura JVM y estrategia de asignación de memoria uno (caso de desbordamiento de memoria de área de método, pila, montón de JVM)


Prefacio

Java y C ++ tienen directamente un muro alto rodeado de asignación de memoria dinámica y tecnología de recolección de basura. Las personas que están fuera del muro quieren entrar y las personas que están dentro del muro quieren entrar. Como ingeniero de Java, lo llevaré a explorar los secretos de la pared.
Este artículo es el primer artículo del aprendizaje del módulo recolector de basura y estrategia de asignación de memoria de JVM. Se utiliza principalmente como un pavimento antes del aprendizaje real. Principalmente presenta el área de datos de tiempo de ejecución de JVM y los escenarios bajo los cuales ocurre el desbordamiento de memoria en cada área. A continuación, se simularán y analizarán los escenarios de desbordamiento de memoria de varias áreas de la JVM a través del combate de código real.


Área de datos de tiempo de ejecución de JVM

Inserte la descripción de la imagen aquí

Contador de programa

El contador de programa es una pequeña área de memoria que se utiliza para registrar la dirección de las instrucciones de código de bytes de la máquina virtual que está ejecutando el subproceso actual. En el caso del subproceso múltiple, para que el subproceso actual se recupere rápidamente a la última posición del código ejecutado después de adquirir recursos de CPU, cada subproceso debe tener su propio contador de programa separado, y los contadores de programa entre cada subproceso no se afectan entre sí, Independiente almacenamiento, por lo que el contador del programa pertenece al área de datos de aislamiento de subprocesos. Esta área es también el único lugar donde no se menciona OutOfMemoryError en la "Especificación de la máquina virtual Java".

Pila de máquinas virtuales

La pila de la máquina virtual también es privada para el subproceso. Cada subproceso tendrá una pila de máquina virtual. El ciclo de vida es el mismo que el del subproceso. Si el subproceso actual no llama a un método, se creará un marco de pila para almacenar la tabla de variables locales, la pila de operandos, el enlace dinámico y el método. Para la exportación y otra información, el proceso desde que se llama y ejecuta cada método hasta la finalización de la ejecución corresponde al proceso de empujar este marco de pila en la pila de la máquina virtual a saliendo de la pila.
Se definen dos excepciones para esta área en la "Especificación de la máquina virtual Java": 1. Si la profundidad de la pila solicitada por el subproceso es mayor que la profundidad máxima permitida por la máquina virtual, se devolverá una
excepción Stack OverflowError; 2 . En apoyo de la dinámica de la pila En la máquina virtual expandida, si no puede solicitar suficiente memoria durante la expansión automática de la pila, se devolverá OutOfMemoryError (si la máquina virtual actual no admite la expansión dinámica de la pila, se lanzará un StackOverflowError cuando el espacio restante en la pila no puede crear un nuevo marco de pila)

Pila de métodos nativos

La pila del método nativo es similar a la pila de la máquina virtual, la pila de la máquina virtual es para el método java y la pila del método local es para el método nativo.

montón

El montón es la parte más grande de memoria administrada por la máquina virtual. Es un área de memoria compartida por subprocesos. Se crea cuando la máquina virtual comienza a almacenar instancias de objetos creadas durante la ejecución de la máquina virtual. También es un área clave para la recolección de basura y la fragmentación de la memoria. La nueva generación, la vieja generación, el área de Edén, etc., que se mencionan en la figura anterior no son la división más detallada del montón en la "Especificación de la máquina virtual de Java", sino la división lógica del montón basada en el diseño generacional de la basura. recolector. Desde la perspectiva del diseño del recolector de basura, la división detallada del montón en la figura anterior está abierta a discusión. Esta sección realmente analizará los diferentes recolectores de basura en detalle más adelante. Desde la perspectiva de la asignación de memoria, el montón compartido por todos los subprocesos también se puede dividir en varios búferes de asignación privados de subprocesos (TLAB) para mejorar la eficiencia de la asignación de objetos. El propósito de dividir el montón de Java es recuperar mejor la memoria y asignar memoria más rápido.

Área de métodos

El área de método, como el montón, también es un área de memoria compartida por subprocesos, que se utiliza principalmente para almacenar datos como información de tipo, constantes y variables estáticas cargadas por la JVM. Hablando del área de método, se debe mencionar el concepto de "generación permanente". Antes de JDK8, la generación permanente se usaba en la máquina virtual HotSpot para implementar el área de método, es decir, el área de método se coloca en el heap, lo que ahorra la necesidad de escribir una recolección de basura separada y administración de memoria para el área de métodos. Sin embargo, este diseño hace que la máquina virtual sea más propensa al desbordamiento de memoria. El código permanente de referencia tiene un límite superior de -XX: MaxPerSize, incluso si no está configurado, Habrá un límite superior predeterminado y, a medida que el grupo constante se vuelva cada vez más constante, se cargan cada vez más tipos de información, y debido a que los tipos de condiciones de descarga y reciclaje son más exigentes, habrá una mayor posibilidad de provocar un desbordamiento de la memoria. . Después de JDK8, el metaespacio en la memoria local se usa para implementar el área de método, de modo que el área de método se puede expandir dinámicamente de forma infinita siempre que no toque el límite superior de la memoria del sistema operativo. En la "Especificación de máquina virtual de Java" se estipula que se lanzará la excepción OutOfMemoryError cuando el área de método no pueda cumplir con los requisitos de asignación de memoria.

Memoria directa

Estrictamente hablando, la memoria directa no es parte del área de datos de tiempo de ejecución de jvm, ni es el área de memoria definida en la "Especificación de la máquina virtual de Java". Pero esta parte del área de memoria también se usa con frecuencia y también puede hacer que aparezca OutOfMemoryError. Esta área se utiliza principalmente para almacenar el Socket. Cuando el programa java necesita comunicarse con otros servicios como la base de datos, necesita crear un Socket para enviar y recibir información. El socket está a nivel del sistema operativo y el jvm recibe la instrucción de conexión de creación del programa java. Luego, se llama a la función del sistema operativo para asignar directamente la memoria fuera del montón, y luego se usa un objeto DirectByteBuffer almacenado en el montón java como referencia a esta memoria para operar. Aunque la memoria directa no está limitada por la máquina virtual, está limitada por el límite superior de la memoria del sistema operativo al igual que el área de método, y también arroja OutOfMemoryError cuando la memoria no cumple con la asignación.

Pila de máquinas virtuales OutOfMemoryError, StackOverflowError

Con respecto a estas dos excepciones en la pila, debe quedar claro bajo qué circunstancias causarán el tipo de excepción:

  1. La pila no admite la expansión dinámica y la profundidad de la pila de solicitudes de la pila de subprocesos supera la profundidad máxima de la pila de la máquina virtual StackOverflowError
  2. La pila admite la expansión dinámica (los escenarios de subprocesos múltiples y los escenarios de un solo subproceso hacen que OutOfMemoryError tenga diferentes ideas de procesamiento) OutOfMemoryError
    PS: La pila admite la expansión dinámica de la máquina virtual en sí, y esta verificación se realiza mejor en un servidor que limita el máximo memoria de un solo proceso, como el sistema operativo Windows 32G; de lo contrario, si la pila admite la expansión dinámica, el tamaño de la pila está limitado por el sistema operativo. Mi computadora tendrá que esperar mucho tiempo para que 16G produzca resultados. Principalmente, no me molesto en descargar Classic y otras máquinas virtuales que admiten la expansión dinámica. Por supuesto, hay un método de ajuste para el segundo escenario: si el OutOfMemoryError se produce en el escenario de un solo subproceso, significa que la profundidad máxima de la pila no cumple con la profundidad requerida por el subproceso cuando la presión interna se aprieta para el extremo. Para aumentar la profundidad de la pila, puede intentar aumentar la profundidad de la pila reduciendo la memoria del montón o reduciendo el tamaño de un solo marco de pila. Si se encuentra en un escenario de subprocesos múltiples, debe ser el límite superior de la memoria y el OOM causado por la incapacidad de crear una pila para un nuevo subproceso En este momento, puede reducir la memoria del montón o reducir la profundidad de una sola pila a cambio de más subprocesos.
    A continuación, verifique que la primera pila de escenarios no admita la expansión dinámica y que la profundidad de la pila de solicitudes de la pila de subprocesos supere la profundidad máxima de la pila de la máquina virtual, lo que genera StackOverflowError.
public class StackSpace {
    
    
    int i=0;
    public void demo(){
    
    
        System.out.println(i);
        i++;
        demo();
    }

    public static void main(String[] args) {
    
    
        StackSpace stackSpace=new StackSpace();
        stackSpace.demo();
    }

}

Resultado de la impresión:

0
...
9866
9867
9868
9869
9870
9871
Exception in thread "main" java.lang.StackOverflowError
	at java.base/java.nio.Buffer.<init>(Buffer.java:222)
	at java.base/java.nio.CharBuffer.<init>(CharBuffer.java:281)
	at java.base/java.nio.HeapCharBuffer.<init>(HeapCharBuffer.java:75)
	at java.base/java.nio.CharBuffer.wrap(CharBuffer.java:393)
	at java.base/sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:280)
	at java.base/sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
	at java.base/java.io.OutputStreamWriter.write(OutputStreamWriter.java:211)
	at java.base/java.io.BufferedWriter.flushBuffer(BufferedWriter.java:120)
	at java.base/java.io.PrintStream.write(PrintStream.java:605)
	at java.base/java.io.PrintStream.print(PrintStream.java:676)
	at java.base/java.io.PrintStream.println(PrintStream.java:812)
	at com.oom.StackSpace.demo(StackSpace.java:9)
	at com.oom.StackSpace.demo(StackSpace.java:11)

堆 OutOfMemoryError

Para hacer que la memoria del montón se desborde más rápido, necesitamos configurar algunos parámetros de la JVM para hacer que la memoria del montón sea más pequeña

**
 * -Xmx20m -Xms20m -Xmn10m -XX:SurvivorRatio=8 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails
 * HeapSpace outOfMemoryError JVM堆内存溢出Demo
 * JVM参数:
 * -Xmx20m 最大堆内存
 * -Xms20m 最小堆内存
 * -Xmn10m 新生代内存
 * -XX:SurvivorRatio=8 新生代中Eden区与Survivor区比值
 * -XX:+HeapDumpOnOutOfMemoryError 堆内存溢出快照
 * -XX:+PrintGCDetails GC日志打印
 */
public class HeapSpace {
    
    
    static class Demo{
    
    

    }

    public static void main(String[] args) {
    
    

        List<Demo> demos=new ArrayList<>();

        while (true){
    
    
            demos.add(new Demo());

        }
    }

}

Resultado de la impresión:

[GC (Allocation Failure) [PSYoungGen: 8192K->1008K(9216K)] 8192K->5121K(19456K), 0.0090997 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 9200K->9200K(9216K)] 13313K->19432K(19456K), 0.0120218 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
[Full GC (Ergonomics) [PSYoungGen: 9200K->0K(9216K)] [ParOldGen: 10232K->9836K(10240K)] 19432K->9836K(19456K), [Metaspace: 3190K->3190K(1056768K)], 0.1015416 secs] [Times: user=0.38 sys=0.01, real=0.10 secs] 
[Full GC (Ergonomics) [PSYoungGen: 7742K->8020K(9216K)] [ParOldGen: 9836K->7725K(10240K)] 17578K->15746K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.1425264 secs] [Times: user=0.77 sys=0.01, real=0.14 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 8020K->8006K(9216K)] [ParOldGen: 7725K->7725K(10240K)] 15746K->15731K(19456K), [Metaspace: 3195K->3195K(1056768K)], 0.1086842 secs] [Times: user=0.65 sys=0.00, real=0.11 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7963.hprof ...
Heap dump file created [27802776 bytes in 0.091 secs]
Heap
 PSYoungGen      total 9216K, used 8192K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 100% used [0x00000007bf600000,0x00000007bfe00000,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 46% used [0x00000007bff00000,0x00000007bff76e30,0x00000007c0000000)
 ParOldGen       total 10240K, used 7725K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 75% used [0x00000007bec00000,0x00000007bf38b770,0x00000007bf600000)
 Metaspace       used 3227K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 358K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:267)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233)
	at java.util.ArrayList.add(ArrayList.java:464)
	at com.oom.HeapSpace.main(HeapSpace.java:27)

Área de método OutOfMemoryError

El área de método es un área que ha cambiado mucho durante el proceso de actualización de JDK6-JDK7-JDK8. Con la aparición de la generación permanente para eliminar el metaespacio, ha habido muchas disputas sobre el área de método, como si la constante de tiempo de ejecución el grupo está en el montón o en el montón. ¿En el metaespacio? ¿También hay una serie de preguntas de la entrevista traídas por el método de pasante de cuerdas? Para estas dos piezas, puedes consultar el artículo del jefe, yo elegí dos artículos que son mejores.

  • ¿El grupo de constantes de tiempo de ejecución está en el montón o en el metaespacio? https://blog.csdn.net/weixin_44556968/article/details/109468386
  • ¿Una serie de preguntas de la entrevista traídas por el método de pasante de cuerdas? https://blog.csdn.net/qq_36426468/article/details/110150453

Memoria directa OutOfMemoryError

Inserte la descripción de la imagen aquí

/**
 * -XX:MaxDirectMemorySize=10m 指定直接内存大小
 */
public class DirectMemory {
    
    
    private static final int _1mb=1024*1024;


    public static void main(String[] args)throws Exception {
    
    

        Field declaredField = Unsafe.class.getDeclaredFields()[0];
        //设置允许暴力访问
        declaredField.setAccessible(true);
        // 传入启动类加载器(启动类加载器是c++实现的,在java代码中为null)
        Unsafe unsafe = (Unsafe) declaredField.get(null);
        while (true){
    
    
            unsafe.allocateMemory(_1mb);
        }
    }
}

El desbordamiento de memoria causado por la memoria directa tiene una característica obvia de que no habrá anomalías obvias en el archivo HeapDump. Si el lector encuentra que el archivo Dump generado después del desbordamiento de memoria es pequeño y el programa usa directa o indirectamente DirectMemory (típicamente el el uso es NIO), puede centrarse en las razones de la memoria directa.

Supongo que te gusta

Origin blog.csdn.net/yangxiaofei_java/article/details/115272721
Recomendado
Clasificación