1. Prefacio
Java es un lenguaje de programación de alto nivel de alto nivel de plataforma de hardware cruzado. Los programas Java se ejecutan en Java Virtual Machines (JVMS) y administran la memoria por JVMS. Esta es la mayor diferencia de C ++. Aunque JVMS administra la memoria, también debemos entender cómo JVM administra la memoria. Actualmente no solo existen un JVM, y puede existir docenas de máquinas virtuales, sino que un diseño de máquina virtual que cumple con la especificación debe seguir la "Especificación de la máquina virtual Java". Este artículo se basa en la descripción de Hotspot Virtual Machine, y se mencionará si hay diferencias con otras máquinas virtuales. Este artículo describe principalmente cómo se distribuye la memoria en JVM, cómo se almacenan y acceden los objetos del programa Java, y posibles excepciones en varias áreas de memoria.
2. Distribución de memoria (región) en JVM
Al ejecutar programas Java, el JVM divide la memoria en múltiples áreas de datos diferentes para la gestión. Estas áreas tienen diferentes funciones, creación y tiempos de destrucción. Algunas áreas se asignan cuando se inicia el proceso JVM, mientras que otras están relacionadas con el ciclo de vida del hilo del usuario (el hilo del programa en sí). Según la especificación JVM, las áreas de memoria administradas por el JVM se dividen en las siguientes áreas de datos de tiempo de ejecución:
1. Pila de máquinas virtuales
Esta área de memoria es privada por el hilo y se crea a medida que el hilo comienza y se destruye cuando se destruye. El modelo de memoria para la ejecución de los métodos Java descritos por la pila de máquinas virtuales: cada método creará un marco de pila (marco de pila) al comienzo de la ejecución, que se utiliza para almacenar tablas variables locales, pilas de operando, enlaces dinámicos, salidas de métodos, etc. La ejecución y retorno de cada método se completan, y se completan una pila de pila en la mancha de máquina virtual.
Como su nombre indica, la tabla de variables locales es un área de memoria que almacena variables locales: almacena los tipos de datos básicos (8 tipos de datos básicos de Java), tipos de referencia y direcciones de retorno que se pueden encontrar durante el período del compilador; Los tipos largos y dobles que ocupan 64 bits ocuparán 2 espacio variable local, y otros tipos de datos solo ocupan 1; Dado que se determina el tamaño del tipo y se puede conocer el número de variables durante el período de compilación, la tabla de variable local tiene un tamaño conocido cuando se crea. Esta parte del espacio de memoria se puede asignar durante el período de compilación, y no es necesario modificar el tamaño de la tabla de la variable local durante la ejecución del método.
En la especificación de la máquina virtual, se especifican dos excepciones para esta área de memoria:
1. Si la profundidad de la pila solicitada por el hilo es mayor que la profundidad permitida (?), Se lanzará StackOverflowError ;
2. Si la máquina virtual puede expandirse dinámicamente, cuando la expansión no puede solicitar una memoria suficiente, se lanzará una excepción de OutOfMemory ;
2. Pila de métodos locales
La pila de métodos locales también es privado de subprocesos, y su función es casi la misma que la pila de máquinas virtuales: la pila de máquinas virtuales proporciona servicios de pila de entrada y salida para la ejecución del método Java, mientras que la pila de métodos locales proporciona servicios para la máquina virtual para ejecutar métodos nativos.
En la especificación de la máquina virtual, no existe una regulación obligatoria en el método de implementación de la pila de métodos locales, y puede ser implementada libremente por la máquina virtual específica; La máquina virtual del punto de acceso combina directamente la pila de máquina virtual y la pila de métodos locales en una; Para que otras máquinas virtuales implementen este método, los lectores pueden consultar información relevante si están interesados;
Al igual que la pila de máquinas virtuales, la pila de métodos locales también arrojará StackOverflowError和OutOfMemory las excepciones.
3. Calculadora del programa
La calculadora del programa también es un área de memoria privada de hilos. Se puede considerar como un indicador de número de línea (apuntando a una instrucción) para que los subprocesos ejecuten bytecode. Cuando se ejecuta Java, obtiene la siguiente instrucción que se ejecutará cambiando el valor del contador. Las órdenes de ejecución de ramas, bucles, saltos, manejo de excepciones, recuperación de hilos, etc., confían en este mostrador para completar. La lectura múltiple de una máquina virtual se logra cambiando a su vez y asignando el tiempo de ejecución del procesador. El procesador (un núcleo para un procesador de múltiples núcleos) solo puede ejecutar un comando a la vez. Por lo tanto, después de que el hilo realiza la conmutación, debe restaurarse a la posición de ejecución correcta. Cada hilo tiene una calculadora de programa independiente.
Al ejecutar un método Java, la calculadora del programa registra (señala) la dirección de la instrucción ByTecode que se está ejecutando el subproceso actual. Si el método nativo se está ejecutando, el valor de esta calculadora está indefinido. Esto se debe a que el modelo de hilo de la máquina virtual del punto de acceso es un modelo de subproceso nativo, es decir, cada hilo de Java asigna directamente el hilo del sistema operativo (sistema operativo). Al ejecutar el método nativo, el sistema operativo lo ejecuta directamente. El valor de este contador de la máquina virtual es inútil; Dado que esta calculadora es un área de memoria con espacio muy pequeño, privado y no requiere expansión. Es la única área en la especificación de la máquina virtual que no especifica ninguna excepción OutOfMemoryError .
4. Memoria de montón (montón)
El montón Java es un área de memoria compartida por los hilos. Se puede decir que es el área de memoria más grande administrada por la máquina virtual y se crea cuando se inicia la máquina virtual. La memoria Java Heap almacena principalmente instancias de objetos, y casi todas las instancias de objetos (incluidas las matrices) se almacenan aquí. Por lo tanto, esta es también el área principal de memoria de la recolección de basura (GC). El contenido sobre GC no se describirá aquí;
Según la especificación de la máquina virtual, la memoria del montón Java puede estar en la memoria física discontinua. Siempre que sea lógicamente continuo y no haya límite en la expansión del espacio, puede ser un tamaño fijo o un árbol extendido. Si la memoria del montón no tiene suficiente espacio para completar la asignación de instancias y no se puede expandir, se lanzará una excepción OutOfMemoryError .
5. Área de método
El área del método es el área de memoria compartida por los subprocesos, al igual que la memoria de Heap, almacena información de tipo, constantes, variables estáticas, código compilado durante el período de compilación instantánea y otros datos. La especificación de la máquina virtual no tiene demasiadas restricciones en la implementación del área del método y, al igual que la memoria de almacenamiento, no requiere espacio de memoria física continua, el tamaño puede ser fijo o escalable, y también se puede elegir para no implementar la recolección de basura; Cuando el área del método no puede cumplir con los requisitos de asignación de memoria, se lanzará la excepción OutOfMemoryError .
6. Memoria directa
La memoria directa no es parte de la memoria administrada de la máquina virtual, pero esta parte de la memoria aún puede usarse con frecuencia; Cuando los programas de Java usan métodos nativos (como NIO, NIO, no se dan descripciones aquí), la memoria puede asignarse directamente fuera de tiempo, pero el espacio de memoria total es limitado, y no habrá memoria insuficiente, y también se lanzará una excepción OutOfMemoryError .
2. Acceso de almacenamiento de objetos de instancia
El primer punto anterior tiene una descripción general de la memoria en cada área de la máquina virtual. Para cada área, hay problemas con cómo se crean, se establecen y acceden los datos. Usemos la memoria de montón más utilizada como ejemplo para hablar sobre estos tres aspectos basados en el punto de acceso.
1. Creación de objetos de instancia
Cuando la máquina virtual ejecuta una nueva instrucción, primero localiza la referencia del símbolo de clase del objeto de creación desde el grupo constante, y juzga si la clase ha sido cargada e inicializada. Si no está cargado, se ejecutará el proceso de inicialización de carga de clase (la descripción no se hará aquí sobre la carga de clase). Si no se puede encontrar esta clase, se lanzará una excepción común ClassNotFoundException ;
Después de la comprobación de carga de clase, la memoria física (memoria de montón) en realidad se asigna al objeto. El espacio de memoria requerido por el objeto está determinado por la clase correspondiente. Después de la carga de clase, el espacio de memoria requerido por el objeto de esta clase se fija; La asignación del espacio de memoria para el objeto es equivalente a dividir una pieza del montón y asignarla a este objeto;
Según si el espacio de memoria es continuo (asignado y no asignado se divide en dos partes completas) se divide en dos formas de asignar memoria:
1. Memoria continua: un puntero se usa como un punto divisivo entre la memoria asignada y no asignada. La asignación de memoria del objeto solo requiere el puntero para mover el tamaño del espacio al segmento de memoria no asignado; Este método se llama "colisión de puntero".
2. Memoria discontinua: la máquina virtual necesita mantener (registrar) una lista que registra esos bloques de memoria en el montón que no se asignan. Al asignar la memoria del objeto, seleccione un área de memoria de tamaño apropiado para asignarla al objeto y actualizar esta lista; Este método se llama "Lista gratuita".
La asignación de la memoria del objeto también encontrará problemas de concurrencia. La máquina virtual utiliza dos soluciones para resolver este problema de seguridad de hilo: primero, use CAS (comparar y establecer)+ para identificar y volver a intentarlo para garantizar la atomicidad de la operación de asignación; En segundo lugar, la asignación de memoria se divide en diferentes espacios de acuerdo con los hilos, es decir, cada hilo pre-asignado una pieza de memoria privada de hilo en el montón, llamado búfer alquitido de subprocesos local (TLAB); Cuando ese hilo quiere asignar memoria, se asigna directamente del TLAB. Solo cuando el TLAB del hilo se asigna después de reasignar, se puede asignar la operación sincrónica desde el montón. Esta solución reduce efectivamente la concurrencia de la memoria del montón de asignación de objetos entre los subprocesos; Si la máquina virtual usa TLAB se establece a través del parámetro JVM -xx: +/- usetLab.
Después de completar la asignación de memoria, además de la información del encabezado del objeto, la máquina virtual inicializa el espacio de memoria asignado al valor cero para garantizar que los campos de la instancia del objeto se puedan usar directamente al valor cero correspondiente al tipo de datos sin asignar valores; Luego, ejecute el método Init para completar la inicialización de acuerdo con el código antes de que se complete la creación de un objeto de instancia;
2. El diseño de objetos en la memoria
En la máquina virtual del punto de acceso, los objetos se dividen en tres partes en la memoria: encabezado de objetos, datos de instancia y alineación y llenado:
El encabezado del objeto se divide en dos partes: parte de él almacena los datos de tiempo de ejecución del objeto, incluido el código hash, la edad de generación de recolección de basura, el estado del bloqueo del objeto, el bloqueo de la retención de rosca, la ID de subproceso sesgada, la marca de tiempo sesgada, etc.; En máquinas virtuales de 32 y 64 bits, esta parte de los datos ocupa 32 y 64 bits respectivamente; Dado que hay muchos datos de tiempo de ejecución, 32 o 64 bits no es suficiente para almacenar completamente todos los datos, por lo que esta parte está diseñada para almacenar datos de tiempo de ejecución en un formato no fijo, pero utiliza diferentes bits para almacenar datos de acuerdo con el estado del objeto; La otra parte almacena el puntero del tipo de objeto, señalando la clase de este objeto, pero esto no es necesario, y los metadatos de clase del objeto no necesariamente deben determinarse utilizando esta parte del almacenamiento (se discutirá a continuación);
Los datos de instancia son el contenido de varios tipos de datos definidos por el objeto, y los datos definidos por estos programas no se almacenan en el orden definido. Se determinan en el orden de las políticas y definiciones de asignación de máquinas virtuales: Long/Double, Int, Short/Char, Byte/Boolean, OOP (Ponint de objeto ordinario) , se puede ver que las políticas se asignan de acuerdo con el número de marcadores de posición del tipo, y los mismos tipos asignarán la memoria juntas; y, bajo la satisfacción de estas condiciones, la subclase precede al orden de las variables de clase principal;
La parte de llenado del objeto no necesariamente existe. Solo juega un papel en la alineación del marcador de posición. En el hotpot, la gestión de memoria de la máquina virtual se gestiona en unidades de 8 bytes. Por lo tanto, cuando se asigna la memoria, el tamaño del objeto no es un múltiplo de 8 y el llenado de alineación se completa;
3. Acceso de objetos <Br /> En el programa Java, creamos un objeto y, de hecho, obtenemos una variable de tipo de referencia, a través de la cual realmente operamos una instancia en la memoria Heap; En la especificación de la máquina virtual, solo se estipula que el tipo de referencia es una referencia que apunta al objeto, y no especifica cómo esta referencia localiza y accede a las instancias en el montón; Actualmente, en las máquinas virtuales convencionales, hay dos formas principales de implementar el acceso a los objetos:
1. Método del mango: una región se divide en la memoria de montón como una piscina de mango. La variable de referencia almacena la dirección de identificación del objeto y el mango almacena la información de dirección específica del objeto de muestra y el tipo de objeto. Por lo tanto, el encabezado del objeto no puede contener el tipo de objeto:
2. Acceso directo al puntero: el tipo de referencia almacena directamente la información de dirección del objeto de instancia en el montón, pero esto requiere que el diseño del objeto de instancia contenga el tipo de objeto:
Estos dos métodos de acceso tienen sus propias ventajas: cuando se cambia la dirección del objeto (clasificación de memoria, recolección de basura), el objeto de acceso al identificador, la variable de referencia no necesita cambiarse, pero solo se cambia el valor de la dirección de objeto en el mango; Al usar el método de acceso directo del puntero, todas las referencias de este objeto deben modificarse; Pero el método del puntero puede reducir una operación de direccionamiento, y en el caso de una gran cantidad de accesos de objetos, las ventajas de este método son más obvias; La máquina virtual de Hotspot utiliza este método de acceso directo de puntero.
3. Excepción de memoria de tiempo de ejecución
Hay dos excepciones principales que pueden ocurrir cuando se ejecutan en el programa Java: OutOfMemoryError y StackOverflowerror; ¿Qué pasará en esa área de memoria? Como se mencionó brevemente antes, a excepción del contador del programa, se producirán otras áreas de memoria; Esta sección demuestra principalmente las excepciones en cada área de memoria a través del código de instancia, y se utilizarán muchos parámetros de inicio de máquina virtual comúnmente utilizados para explicar mejor la situación. (Cómo ejecutar el programa con parámetros no se describe aquí)
1. Desbordamiento de la memoria del montón de Java
El desbordamiento de la memoria del montón ocurre cuando se crean objetos después de que la capacidad del montón alcanza la capacidad máxima de montón. En el programa, los objetos se crean continuamente y se garantiza que estos objetos no se recolecten basura:
/** * Parámetros de la máquina virtual: * -xms20m capacidad mínima del montón * -xmx20m capacidad máxima del montón * @author hwz * */public class headOutOfMemoryError {public static void main (string [] args) {// use el contenedor para guardar el objeto para asegurarse de que el objeto no sea la lista de basura recolectada <méutica de headmoryerror> listtoholdObj ArrayList <HeadoUtoutOfMemoryError> (); while (true) {// crea continuamente objetos y agrégalos a la lista de contenedores Toholholhobj.add (New HeadOutOfMemoryError ()); }}} Puede agregar parámetros de máquina virtual :-XX:HeapDumpOnOutOfMemoryError . Al enviar una excepción OOM, deje que la máquina virtual voltee el archivo de instantánea del montón actual. Puede usar este problema de excepción de segmentación de palabras de archivo en el futuro. Esto no se describirá en detalle. Escribiré un blog para describir en detalle utilizando la herramienta MAT para analizar los problemas de memoria.
2. Pila de máquinas virtuales y desbordamiento de la pila de métodos locales
En la máquina virtual del punto de acceso, estas dos pilas de métodos no se implementan juntas. Según la especificación de la máquina virtual, estas dos excepciones ocurrirán en estas dos áreas de memoria:
1. Si el hilo solicita la profundidad de la pila mayor que la profundidad máxima permitida por la máquina virtual, arroje una excepción de stackoverflowror;
2. Si la máquina virtual no puede aplicar un gran espacio de memoria al expandir el espacio de la pila, se lanzará una excepción OutOfMemoryError;
En realidad, hay una superposición entre estas dos situaciones: cuando el espacio de la pila no se puede asignar, ¿es imposible distinguir si la memoria es demasiado pequeña o la profundidad de la pila usada es demasiado grande?
Use dos formas de probar el código
1. Use el parámetro -xss para reducir el tamaño de la pila, llamar a un método infinitamente recursivamente y aumentar la profundidad de la pila infinitamente:
/** * Parámetros de la máquina virtual: <br> * -xss128k Capacidad de la pila * @author hwz * */public class stackoverflowerror {private int stackdeep = 1; / *** Recurre infinita, amplíe infinitamente la profundidad de la pila de llamadas*/ public void recursiveInvoke () {stackdeep ++; recursiveInvoke (); } public static void main (string [] args) {stackoverflowerror soe = new stackOverFlowerRor (); intente {Soe.recursiveInvoke (); } catch (lanzable e) {system.out.println ("stack profundo =" + soe.stackdeep); tirar E; }}} Se definen una gran cantidad de variables locales en el método, la longitud de la tabla de variables locales en la pila de métodos también se llama infinitamente recursivamente:
/** * @author hwz * */public class stackoomeError {private int stackdeep = 1; / *** Defina una gran cantidad de variables locales, aumente la tabla de variables locales en la pila* recurre infinita, aumente infinitamente la profundidad de la pila de llamadas*/ public void recursiveInvoke () {double i; Doble i2; //............ La gran cantidad de definiciones variables se omiten aquí StackDeep ++; recursiveInvoke (); } public static void main (string [] args) {stackoomeError soe = new stackOomeError (); intente {Soe.recursiveInvoke (); } catch (lanzable e) {system.out.println ("stack profundo =" + soe.stackdeep); tirar E; }}}La prueba de código anterior muestra que no importa si la pila de cuadros es demasiado grande o la capacidad de la máquina virtual es demasiado pequeña, cuando la memoria no se puede asignar, se lanza todo StackOverflowerror;
3. Método Área y tiempo de ejecución Desbordamiento de la piscina constante
Aquí primero describiremos el método interno de cadena: si el grupo constante de cadena ya contiene una cadena igual a este objeto de cadena, devolverá un objeto de cadena que represente esta cadena. De lo contrario, agregue este objeto de cadena al grupo constante y devuelva una referencia a este objeto de cadena; A través de este método, agregará continuamente un objeto de cadena al grupo constante, lo que resulta en desbordamiento:
/** * Parámetros de la máquina virtual: <br> * -xx: permSize = 10m Tamaño del área permanente * -xx: maxPermsize = 10m Área permanente Capacidad máxima * @author Hwz * */public class runtimeNstancePoOloom {public static void main (string [] args) {// usa contenedor para ahorrar el objeto para asegurarse de que el objeto no sea el garbagbage Collect Classatic. ArrayList <String> (); // Use el método String.Intern para agregar el objeto del grupo constante para (int i = 1; true; i ++) {list.add (String.ValueOf (i) .intern ()); }}}Sin embargo, este código de prueba no se desborda durante el grupo constante de tiempo de ejecución en JDK1.7, pero sucederá en JDK1.6. Por esta razón, escriba otro código de prueba para verificar este problema:
/** * El método String.Intern se prueba bajo diferentes jdks * @author hwz * */public class StringInternTest {public static void main (string [] args) {string str1 = new StringBuilder ("test"). Append ("01"). ToString (); System.out.println (str1.intern () == str1); String str2 = new StringBuilder ("Test"). Append ("02"). ToString (); System.out.println (str2.intern () == str2); }} Los resultados de la ejecución bajo JDK1.6 son: falso, falso;
El resultado de correr bajo JDK1.7 es: Verdadero, verdadero;
Resulta que en JDK1.6, el método Intern () copia la primera instancia de cadena encontrada a la generación permanente, que a su vez es una referencia a la instancia en la generación permanente, y las instancias de cadena creadas por StringBuilder están en el montón, por lo que no son iguales;
En JDK1.7, el método Intern () no copia la instancia, sino que solo registra la referencia de la primera instancia que aparece en el grupo constante. Por lo tanto, la referencia devuelta por Intern es la misma que la instancia creada por StringBuilder, por lo que devuelve verdadero;
Por lo tanto, el código de prueba para el desbordamiento constante de la piscina no tendrá una excepción de desbordamiento constante de la piscina, pero puede tener una excepción de desbordamiento de memoria de montón insuficiente después de una ejecución continua;
Luego, debe probar el desbordamiento del área del método, solo seguir agregando cosas al área del método, como nombres de clases, modificadores de acceso, piscinas constantes, etc. Podemos permitir que el programa cargue una gran cantidad de clases para llenar continuamente el área del método, lo que conduce al desbordamiento. Usamos CGLIB para manipular directamente el Bytecode para generar una gran cantidad de clases dinámicas:
/** * Método Memoria Memoria ¡Clase de prueba de desbordamiento * @author hwz * */public class MethodAreaoom {public static void main (string [] args) {// Use GCLIB para crear subclases infinitamente mientras (verdadero) {potencador potencador = new mejor (); potencador.setsuperClass (maoomclass.class); potencador.setUsecache (falso); mejoran.setCallback (new MethodInterceptor () {@Override Public Object Intercept (Object obj, Method Method, Object [] Args, MethodProxy Proxy) lanza lando {return proxy.invokesuper (obj, args);}}); potencador.create (); }} clase estática MaoomClass {}} A través de la observación de VisualVM, podemos ver que el número de clases cargadas de JVM aumenta en línea recta con el uso de Pergen:
4. Desbordamiento de la memoria directa
El tamaño de la memoria directa se puede establecer a través de los parámetros de la máquina virtual : -xx: maxDirectMemorySize . Para hacer desbordamiento de memoria directa, solo necesita solicitar continuamente la memoria directa. La siguiente es la misma que la prueba de caché de memoria directa en Java NIO:
/** * Parámetros de la máquina virtual: <br> * -xx: maxDirectMemorySize = 30m Tamaño de memoria directa * @author hwz * */public class DirectMemoryoom {public static void main (String [] args) {list <buffer> buffers = new ArrayList <Buffer> (); int i = 0; while (true) {// imprime el sistema actual.out.println (++ i); // Consumo de memoria directa aplicando continuamente el consumo de memoria del búfer directo en el búfer de caché.add (bytebuffer.allocatedirect (1024*1024)); // contabilidad 1m cada vez}}} En el bucle, cada vez que se aplica la memoria directa de 1M, la memoria directa máxima se establece en 30m y se lanza una excepción cuando el programa se ejecuta 31 veces: java.lang.OutOfMemoryError: Direct buffer memory
4. Resumen
Lo anterior es todo el contenido de este artículo. Este artículo describe principalmente la estructura de diseño de la memoria, el almacenamiento de objetos y las excepciones de memoria que pueden ocurrir en varias áreas de memoria en el JVM; El libro de referencia principal "Comprensión en profundidad de Java Virtual Machine (segunda edición)". Si hay alguna incorrección, apírtela en los comentarios; Gracias por su apoyo a Wulin.com.