Historia de JVM: mecanismo de carga de clases de máquinas virtuales

Mecanismo de carga de clases de máquinas virtuales


I. Descripción general

Este capítulo explicará cómo los archivos de clase ingresan a la máquina virtual y cómo la máquina virtual procesa estos archivos de clase. La máquina virtual Java carga el archivo de clase en la memoria, verifica, convierte, analiza e inicializa los datos y finalmente forma un tipo Java que puede ser utilizado directamente por la máquina virtual. Este proceso se denomina mecanismo de carga de clases de la máquina virtual. máquina. Todos estos procesos ocurren mientras el programa se está ejecutando, lo que le da a Java una escalabilidad y flexibilidad extremadamente altas.

2. Momento de carga de clases

Desde que se carga en la memoria de la máquina virtual hasta que se descarga de la memoria, el ciclo de vida de una clase pasará por carga, verificación, preparación, análisis, inicialización, uso y descarga. Entre ellas, las etapas de verificación, preparación y análisis se denominan colectivamente proceso de conexión.
Insertar descripción de la imagen aquí
Sin embargo, estas etapas no están necesariamente en el orden que se muestra en la figura. Por ejemplo, el comienzo de la etapa de análisis puede ser después de la inicialización, esto es para admitir el enlace dinámico de Java. La razón por la que digo "comenzar" aquí, en lugar de "terminar", es que las distintas etapas están entrelazadas entre sí, en lugar de esperar el final de la etapa anterior antes de pasar a la siguiente.
La especificación de la máquina virtual Java no restringe cuándo se carga una clase, pero existen restricciones estrictas sobre la inicialización de una clase. La clase se inicializará solo en las siguientes seis situaciones: (1) Cuando son new, getstatic, putstatic o invokestatic
encontrado Durante estas cuatro instrucciones de código de bytes, si el tipo no se inicializa, se inicializa. Los escenarios específicos son: utilizar la nueva palabra clave para crear una instancia de un objeto; leer o establecer un campo estático (excepto el modificado por final, que se ha puesto en el grupo constante en el momento de la compilación); cuando se llama a un método estático de un tipo (2) usar
java Cuando el método del paquete .lang.reflect realiza una llamada de reflexión en un tipo, si el tipo no está inicializado, se inicializará.
(3) Al inicializar una clase, si su clase principal no se ha inicializado, su clase principal se inicializará primero
(4) Cuando se inicia la máquina virtual, la máquina virtual primero inicializará la clase principal que se ejecutará especificada por el usuario
( 5) Si un java.lang El resultado final del análisis de la instancia .invoke.MethodHandle son cuatro tipos de identificadores de método: REF_getStatic, REF_putStatic, REF_invokeStatic y REF_newInvokeSpecial. Si la clase correspondiente al identificador del método no se ha inicializado, su inicialización es (6) Define el nuevo valor predeterminado agregado por
el método JDK 8 (método de interfaz modificado por la palabra clave predeterminada), la interfaz debe inicializarse antes que la clase de implementación de la interfaz.
Para el tercer tipo, las interfaces y las clases son diferentes. Las interfaces no requieren que todas sus interfaces principales estén inicializadas. Solo se inicializarán cuando la interfaz principal se utilice realmente.

3. Proceso de carga de clases

(1) Carga
La carga es una etapa de carga de toda la clase. Durante la etapa de carga, toda la máquina virtual Java debe completar las siguientes tres cosas:
1. Obtener el flujo de bytes binarios que define esta clase a través del nombre completo de una clase.
2. El resultado del almacenamiento estático representado por este flujo de bytes se convierte en una estructura de datos en tiempo de ejecución en el área de método
3. Genere un objeto java.lang.class que represente esta clase en la memoria, que represente varias entradas de acceso a datos de esta clase en el área de métodos
Estas definiciones no son particularmente específicas, lo que deja mucho espacio para las máquinas virtuales y Java. Por ejemplo, este tipo de flujo de bytes binarios no solo se puede obtener a través de archivos de clase, sino que también se puede obtener a partir de archivos ZIP (JAR, WAR, EAR), obtenerlos de la red (WEB Applet), obtenerlos de la base de datos y calcularlos. en tiempo de ejecución. Generar (proxy dinámico), obtener de otros archivos (JSP genera el archivo de clase correspondiente).
Para el proceso de obtención del flujo de bytes binarios de la clase, la etapa de carga sin tipo de matriz es la etapa más controlable. puedo usar virtual Esto se puede lograr usando el cargador de clases de arranque integrado de la máquina, o puede definir su propio cargador de clases para obtener el flujo de bytes binarios.
La clase de matriz en sí no se crea a través del cargador de clases, sino que la máquina virtual Java la crea dinámicamente directamente en la memoria. Sin embargo, su carga todavía está estrechamente relacionada con el cargador de clases, porque los tipos de elementos en la matriz aún dependen del Cargador de clases para cargar.
Si los elementos de la matriz siguen siendo tipos de referencia, busque más y marque la matriz en el espacio de nombres del cargador de clases que carga el tipo de componente hasta que no sea un tipo de referencia. La máquina virtual Java marca la matriz como asociada con el cargador de clases de arranque.
Una vez completada la fase de carga, el flujo de bytes binarios fuera de la máquina virtual Java se almacena en el área de método de acuerdo con el formato establecido por la máquina virtual. Una vez que los datos de tipo se colocan correctamente en el área de método, se creará una instancia de Java en la memoria dinámica de Java. Un objeto de la clase .lang.Class, este objeto servirá como interfaz externa para que el programa acceda a los datos de tipo en el área de métodos.

(2) Verificación La
verificación es el primer paso en la fase de conexión. Este paso es para garantizar que los códigos de bytes de los archivos de clase cumplan con los requisitos de restricción y no dañen la JVM. El lenguaje Java en sí es un lenguaje de programación relativamente seguro. Si hace algo que accede fuera de los límites de una matriz o salta a una línea de código inexistente, el compilador se negará a compilar y generará una excepción. Pero no todos los archivos de clase se compilan desde Java. Los archivos de clase se pueden escribir como cualquier cosa, por lo que la máquina virtual Java necesita verificar el flujo de bytes de entrada. Hay aproximadamente cuatro etapas de acción en la verificación: verificación del formato de archivo, verificación de metadatos, verificación de código de bytes y verificación de referencia de símbolos.
1. Verificación del formato de archivo: consiste en verificar si el formato del archivo de código de bytes cumple con el estándar:
si comienza con el número mágico 0XCAFEBABE
, si los números de versión mayor y menor están dentro del rango aceptable de la máquina virtual, si
el El tipo de constante en el grupo de constantes
apunta correctamente a los distintos índices de la constante. Ya sea que el valor apunte a una constante inexistente o a una constante de un tipo irrazonable.
Hay muchos puntos de verificación en esta etapa. El objetivo principal es garantizar que el El flujo de bytes de entrada se puede analizar y almacenar correctamente en el área del método. Esta etapa se basa en el flujo de bytes binarios. Después de esta etapa, el flujo de bytes puede ingresar al área de método de la memoria de la máquina virtual para su almacenamiento. La verificación posterior se basa en la estructura de almacenamiento del área de método y no será leer., operar el flujo de bytes.
2. Verificación de metadatos: realiza principalmente análisis semántico de la información de descripción del código de bytes. Verificación principal:
si esta clase tiene una clase principal (todo excepto java.lang.object debe tener una clase principal) Si la
clase principal de esta clase hereda una clase que no se permite heredar (clase modificada final)
Si la clase es no es una clase abstracta, si se han implementado todos los métodos requeridos en su clase principal o clase abstracta.
3. Verificación de código de bytes: esta etapa verifica principalmente si la semántica del programa es legal y lógica. Verifica principalmente el cuerpo del método de la clase (el atributo de código del archivo de clase) para garantizar que el método de la clase no realice acciones que pongan en peligro la seguridad de la máquina virtual durante el tiempo de ejecución, como:
Asegúrese de que el tipo de datos de la pila de operandos y la secuencia del código de instrucción puedan funcionar juntos en cualquier momento.
Asegúrese de que ninguna instrucción salte a instrucciones fuera del área del método.
Asegúrese de que la conversión de tipos sea legal. Por ejemplo, puede asignar objetos de subclase al tipo de datos de la clase principal, pero no es seguro asignar un objeto de clase principal a un tipo de datos de subclase.
Si el código de bytes del cuerpo de un método en un tipo no pasa la verificación del código de bytes, debe haber algún problema con él. Pero si se pasa la verificación del código de bytes, es posible que no haya ningún problema. Porque es imposible utilizar el programa para comprobar si la lógica de un programa es correcta.
Para evitar perder demasiado tiempo en la verificación del código de bytes, JDK6 agregó un nuevo atributo "StackMapTable" a la tabla de atributos del atributo Código del cuerpo del método. Estos métodos se verifican cuando se compila el compilador javac y luego se marca StackMapTable. Sin embargo, , todavía existen riesgos de seguridad de esta manera y StackMapTable también puede modificarse antes de ingresar a la máquina virtual.
4. Verificación de referencia simbólica: esta etapa ocurre cuando la máquina virtual convierte referencias simbólicas en referencias directas, y esta acción de conversión ocurre en la etapa de análisis. La verificación de referencia de símbolos puede considerarse como una verificación coincidente de información diversa además de la clase misma. Generalmente es necesario verificar: ¿
se puede encontrar la clase correspondiente a través del nombre completo
? Si hay un descriptor de campo que coincida con el método en la clase especificada y la referencia del método y símbolo de campo descritos por el nombre simple. La verificación
es principalmente para asegúrese de que la fase de análisis pueda ejecutarse normalmente. De lo contrario, se generará una excepción, normalmente java.lang.IllegalAccessError, java.lang.NoSuchFieldError, java.lang.NoSuchMethodError.
La fase de verificación es importante, pero no necesaria. Si un programa es usado y verificado repetidamente. También puede usar -Xverify:none para desactivar la verificación y acortar el tiempo que tarda la máquina virtual en cargar las clases.

(3) Preparación
La etapa de preparación es el proceso de asignar memoria y asignar valores iniciales a las variables de la clase (variables modificadas estáticamente). Según el concepto, el espacio asignado para estas variables debe estar en el área de método, pero el área de método es solo un concepto. Después de JDK8, las variables de clase se almacenarán en el montón de Java junto con el objeto de clase.
La asignación de memoria en esta etapa solo incluye variables de clase (variables estáticas), no variables de instancia. Las variables de instancia se asignarán en el montón de Java junto con el objeto cuando se cree una instancia del objeto. El valor inicial asignado a una variable de clase es generalmente 0.
Insertar descripción de la imagen aquí
Como se muestra en la figura, el valor asignado al valor en la fase de preparación es 0, porque no se ha ejecutado ningún método Java en este momento. El valor asignado al valor es 123. Sí, la instrucción putstatic se compila y se coloca en el constructor de la clase. () método. Debe inicializarse hasta la clase. La asignación se realizará solo en esta etapa.
Insertar descripción de la imagen aquí
Pero si la variable modificada final (constante) es como la imagen de arriba, se le asignará un valor de 123 durante la fase de preparación. Al compilar, Javac generará el atributo ConstantValue para el valor y, en la fase de preparación, el valor se asignará en función del valor de ConstantValue.

(4) Análisis
La etapa de análisis es el proceso en el que la máquina virtual Java reemplaza las referencias simbólicas con referencias directas.
Referencia de símbolo: utilice un conjunto de símbolos para describir el objetivo al que se hace referencia. El símbolo puede ser cualquier literal, siempre que se pueda localizar el objetivo. El destino al que se hace referencia no necesariamente ya está cargado en la máquina virtual.
Referencia directa: un identificador que localiza directa o indirectamente el objetivo. El destino al que se hace referencia debe existir en la memoria de la máquina virtual.
La acción de análisis se realiza principalmente en siete tipos de referencias de símbolos: clases o interfaces, campos, métodos de clase, métodos de interfaz, tipos de métodos, identificadores de métodos y calificadores de sitios de llamadas.
1. Resolución de clase o interfaz: supongamos que actualmente estamos en la clase D y queremos resolver una referencia de símbolo N nunca resuelta en una referencia directa a una clase o interfaz C, lo que implica los siguientes tres pasos: (1) Si
C no es un tipo de matriz, entonces la máquina virtual pasa el nombre completo que representa N al cargador de clases de D para cargar la clase C. Durante el proceso de carga, es posible que se carguen otras clases. Debido a la verificación de metadatos, verificación de código de bytes, etc., una falla en cualquier lugar significa una falla en el análisis.
(2) Si C es un tipo de matriz, depende de su tipo de elemento. Si el tipo de elemento es un objeto, cargue el tipo de elemento de matriz de acuerdo con (1) y cargue el tipo ordinario directamente
(3) para completar la carga. También debe verificar si D tiene permiso para acceder a C. Si no, tire java.lang.IllegalAccessError Excepción
2. Análisis de campos: para analizar un campo, primero debe analizar la referencia del símbolo de la clase o interfaz a la que pertenece el campo. Si el análisis es exitoso, continúe con los siguientes pasos (la clase o interfaz a la que pertenece el campo está representada por c):
(1) Si c contiene un campo cuyo nombre simple y descriptor de campo coinciden con el objetivo, el análisis finaliza. .
(2) Si c implementa una interfaz, cada interfaz y su interfaz principal se buscarán recursivamente de acuerdo con la relación de herencia. Si alguna interfaz contiene campos coincidentes, el análisis finaliza.
(3) Si c no es java.lang.object, la clase principal de c se buscará recursivamente de acuerdo con la relación de herencia. Si hay un campo coincidente, el análisis finalizará (4) De lo contrario, la búsqueda fallará y se
Se devolverá la excepción java.lang.NoSuchFieldError
. Si se encuentra, se realizará la verificación de permiso en el campo, y si no hay permiso, se devolverá una excepción java.lang.IllegalAccessError.
Si aparece un campo con el mismo nombre tanto en la clase principal como en la interfaz implementada de una determinada clase, el único campo de acceso aún se puede determinar de acuerdo con las reglas de análisis, pero el compilador javac puede negarse a analizarlo en un archivo de clase.
2. Análisis de métodos: el primer paso del análisis de métodos es el mismo que el análisis de campos, también es necesario analizar la referencia simbólica de la clase o interfaz a la que pertenece el método indexado en el elemento class_index de la tabla de métodos. La clase o interfaz a la que pertenece el método está representada por c. A continuación, se analizará de acuerdo con los siguientes pasos:
(1) Las definiciones de tipo constante de las referencias de símbolos de los métodos de clase y los métodos de interfaz en el archivo de clase se separan. Si se encuentra en la tabla de métodos de la clase que el índice c en class_index es una interfaz, luego lanza una excepción java.lang.IncompatibleClassChangeError.
(2) Si un objetivo cuyo nombre simple y descriptor coinciden con el método se encuentra en la clase c, se hace referencia directa a él
(3) De lo contrario, busque recursivamente en la clase principal de c
(4) De lo contrario, en la interfaz implementada por la clase c y Busque en la interfaz principal. Si hay un método coincidente, significa que c es una clase abstracta y se
generará una excepción java.lang.AbstractMethodError (5). De lo contrario, la búsqueda fallará y se generará una excepción java.lang.NoSuchMethodError. será arrojado.

3. Análisis del método de interfaz: el análisis del método de interfaz también requiere analizar primero la referencia simbólica de la clase o interfaz a la que pertenece el método indexado en el elemento class_index de la tabla de métodos de interfaz. Utilice c para representar esta interfaz. El proceso de análisis es el siguiente:
(1) A diferencia del método de análisis de clases, si se descubre que c es una clase, se generará la excepción java.lang.IncompatibleClassChangeError
(2) Si hay un método correspondiente en la interfaz c, se hará referencia directa
(3) En c Busque en la interfaz principal de c hasta que se encuentre la clase java.lang.Object. Si se encuentra, se hará referencia directa
(4) Dado que la interfaz Java ejecuta herencia múltiple, si hay varias correspondientes Los métodos se encuentran en diferentes interfaces de clase principal de c, se devolverá uno de ellos
(5). De lo contrario, la búsqueda falla y se genera una excepción java.lang.NoSuchMethodError.

(5) Inicialización
La inicialización de clases es el último paso en el proceso de carga de clases. No es hasta la fase de inicialización que la máquina virtual Java comienza a ejecutar el código del programa Java escrito en la clase. En la fase de inicialización, las variables y otros recursos se inicializan de acuerdo con el plan elaborado por el programador a través de la codificación del programa. La fase de inicialización es el proceso de ejecutar el método constructor de clase (), que es generado automáticamente por el compilador Javac. El compilador fusiona el método () y recopila automáticamente las acciones de asignación de variables en la clase y los bloques de código estático. El orden de recopilación del compilador se basa en el orden de las declaraciones en el archivo fuente. En un bloque de declaraciones estático, solo puede acceder al bloque de declaraciones definido antes y no puede acceder al bloque de declaraciones definido después, pero puede asignar valores. a los bloques de declaración posteriores.
La máquina virtual Java garantiza que la clase principal se haya ejecutado antes de que se ejecute el método () de la subclase, por lo que el método () de java.lang.object debe ejecutarse primero.
Dado que el método () de la clase principal se ejecuta primero, el bloque de declaración estática definido en la clase principal tendrá prioridad sobre la operación de asignación de variables de la subclase.
Insertar descripción de la imagen aquí
Como se muestra en la imagen, el valor de B debería ser 2 en lugar de 1.
Si no hay bloques de declaraciones estáticas ni declaraciones de asignación de variables en una clase o interfaz, el compilador no necesita generar un método () para esta clase. A diferencia de las clases, ejecutar el método ()
de una interfaz no requiere ejecutar primero ( ) método de la interfaz principal.Método, la interfaz principal se inicializará solo cuando se utilicen las variables definidas por la interfaz principal.

4. Cargador de clases

El equipo de diseño de la máquina virtual JAVA coloca intencionalmente la acción de "cargar el flujo de bytes binarios de la clase según su nombre completo" durante el proceso de carga de clases fuera de la máquina virtual Java, para que la aplicación pueda decidir cómo obtener la información requerida. .Class, el código que implementa esta acción se llama "cargador de clases".
(1)
La clase y el cargador de clases determinan conjuntamente la unicidad de la clase en la máquina virtual Java. Incluso si dos clases se originan en el mismo archivo de clase y son cargadas por la misma máquina virtual Java, siempre que los cargadores de clases que las carguen sean diferentes, las dos clases definitivamente no son iguales. La igualdad aquí incluye los resultados devueltos por el método equals(), el método isAssignableFrom() y el método isInstance() del objeto Class que representa la clase.
Insertar descripción de la imagen aquí
En la figura, se construye un cargador de clases simple, que carga una clase y crea una instancia del objeto de esta clase. Desde la primera línea de salida, podemos ver que este objeto de hecho es instanciado por esta clase, pero desde la segunda línea, encontramos que la verificación de tipo entre este objeto y la clase devuelve falso. Esto se debe a que hay dos pruebas de carga de clases, una es una máquina virtual La clase de aplicación se carga y la otra la carga nuestro cargador de clases personalizado. Las dos son dos clases diferentes en la máquina virtual Java.

(2) Modelo de delegación parental
Desde la perspectiva de la máquina virtual Java, solo hay dos cargadores de clases, uno es el cargador de clases de inicio y el otro son todos los demás cargadores de clases.
Pero desde la perspectiva de un desarrollador de Java, los cargadores de clases deberían dividirse en más detalles. Java siempre ha mantenido un cargador de clases de tres niveles y una arquitectura de carga de clases delegada por los padres. La mayoría de los programas Java utilizarán los siguientes tres cargadores de clases proporcionados por el sistema para la carga:
Cargador de clases de inicio: responsable de cargar el directorio <JAVA_HOME>\lib o las clases almacenadas en la ruta especificada por el parámetro -Xbootclasspath.
Cargador de clases de extensión: Es responsable de cargar todas las bibliotecas de clases en el directorio <JAVA_HOME>\lib\ext, o en la ruta especificada por la variable de sistema java.ext.dirs.
Cargador de clases de aplicaciones: responsable de cargar todas las bibliotecas de clases en la ruta de clases del usuario (ClassPath), los desarrolladores también pueden usar este cargador de clases directamente en el código. Si el programa no define su propio cargador de clases, este generalmente es el cargador de clases predeterminado en el programa.
Insertar descripción de la imagen aquí
La relación jerárquica entre varios cargadores de clases que se muestra en la Figura 7-2 se denomina "modelo de delegación parental" de cargadores de clases. Este no es un modelo vinculante, sino la mejor implementación de un cargador de clases recomendada por los diseñadores de Java a los desarrolladores.
El flujo de trabajo del modelo de delegación principal es: cuando el cargador de clases recibe una solicitud para cargar una clase, primero delegará la solicitud al cargador de clases principal para que la complete. Solo cuando el cargador de clases principal final no pueda completarla, lo hará el cargador de clases secundario. completarlo..
Un beneficio obvio de utilizar el modelo de delegación principal es que las clases en Java tienen una relación jerárquica priorizada con sus cargadores de clases. Por ejemplo, la clase java.lang.Object eventualmente será cargada por el cargador de clases principal superior (cargador de clases de inicio) de acuerdo con el modelo de delegación principal. Incluso si el usuario escribe una clase java.lang.obejct, solo se puede compilar normalmente y no se puede cargar ni ejecutar.
Insertar descripción de la imagen aquí
La lógica de este código es: primero verifique si la clase está cargada. Si no, llame al método loadClass() del cargador principal. Si la clase principal está vacía, use el cargador de clases de inicio predeterminado para cargar. Si la clase principal falla para cargar, luego use su propio findClass() para cargar.

(3) Destrucción del modelo de delegación parental
Hasta la llegada de la modularidad de Java, el modelo de delegación parental había sido "destruido" a gran escala tres veces.
La primera vez que se rompió: ocurrió antes de la llegada del modelo de delegación parental, es decir, antes de JDK1.2. El concepto de cargador de clases y java.lang.ClassLoader existía en la primera versión de Java antes de la llegada del modelo de delegación parental. Frente al código existente del cargador de clases definido por el usuario, se deben hacer algunos compromisos al hacer referencia al modelo de delegación principal. Solo podemos intentar guiar a los usuarios para que reescriban el método de carga en el método findClass en lugar del método loadClass.
La segunda vez se rompió: en el modelo de delegación parental, las clases más básicas las carga el cargador de clases de nivel superior. Pero si el tipo básico llama al código de usuario (como el servicio JNDI), el cargador de clases de inicio no reconocerá el código de usuario. Para resolver este dilema, tuvimos que introducir un diseño menos elegante: Thread Context ClassLoader. Puede utilizar el cargador de clases de contexto de subprocesos para cargar el código relacionado con la interfaz del proveedor de servicios. De hecho, el cargador de clases principal solicita al cargador de clases secundario que complete la carga de clases. Este comportamiento viola los principios generales del modelo de delegación principal.
La tercera vez fue destruido: fue causado por la búsqueda por parte del usuario de la naturaleza dinámica del programa, como la implementación en caliente del módulo y el reemplazo en caliente del código.
Una de las propuestas estándar para la implementación en caliente es OSGi. La clave para la implementación en caliente modular de OSGi es la implementación de su mecanismo de cargador de clases personalizado. En el entorno OSGi, el cargador de clases ya no busca según el modelo de delegación principal, sino según el modelo de malla.


Supongo que te gusta

Origin blog.csdn.net/weixin_45841848/article/details/132594533
Recomendado
Clasificación