[JVM] - [Comprensión profunda de las notas de estudio de la máquina virtual Java] -Capítulo 7 Mecanismo de carga de clases de máquina virtual

Descripción general

El mecanismo de carga de clases de la máquina virtual se refiere a que la máquina virtual Java carga los datos que describen la clase desde el archivo de clase en la memoria, verifica los datos, los convierte, los analiza, los inicializa y finalmente forma un tipo Java que puede ser utilizado directamente por la máquina virtual.

Tiempo de carga de clases

Desde el momento en que se carga una clase en la memoria de la máquina virtual hasta el momento en que se descarga de la memoria, todo su ciclo de vida pasará por Loading Loading .C o a d in g ),验证(V erificación VerificaciónV er i f i c a t i o n ),准备(P reparación PreparaciónP re p a r a t i o n ),解析(R esolución ResoluciónR eso l u t i o n ),初始化(I nicialización InicializaciónI ni t ia l i z a c i o n ),使用(U sing UsandoUso ) ydescarga ( DescargaDescarga _Descarga ) siete etapas ,
entre las cuales las tres partes de verificación , preparación y análisis se denominan colectivamenteconectar( Vinculación VinculaciónVinculación )
Ciclo de vida de clase
El orden de estas cinco etapas de carga, verificación, preparación, inicialización y descargaestádeterminado y debeiniciarse paso a paso en este orden.

Situaciones que deben inicializarse

La especificación de la máquina virtual estipula estrictamente que existen y solo las siguientes seis situaciones deben inicializar inmediatamente la clase , por lo que, naturalmente, la carga, la verificación y la preparación comenzarán antes que ella:

  1. Cuando encuentre las cuatro instrucciones de código de bytes new, getstatic, putstatic o invokestatic, si la clase no se ha inicializado, primero debe activar su fase de inicialización. Los escenarios típicos que pueden generar estas cuatro instrucciones son: La nueva palabra clave crea una instancia de un objeto ; lea Get o establecer un campo estático de una clase (excepto el campo estático modificado por final, el resultado se ha puesto en el grupo constante en el momento de la compilación, estos se consideran invariantes globales y tienen poco que ver con la clase); llamar a un campo estático método de una clase cuando
  2. Al realizar una llamada de reflexión a una clase , si la clase no se ha inicializado, es necesario inicializarla primero.
  3. Al inicializar una clase, si descubre que su clase principal no se ha inicializado, primero debe activar la inicialización de su clase principal.
  4. Cuando se inicia la JVM, el usuario debe especificar una clase principal que se ejecutará (la clase que contiene el método main()) y la máquina virtual primero inicializará la clase principal.
  5. Cuando el método predeterminado introducido por JDK 8 se define en una interfaz , si se inicializa la clase de implementación de esta interfaz, la interfaz debe inicializarse antes.

Una clase debe inicializarse naturalmente porque necesita usarse, por lo que los comportamientos anteriores son todos comportamientos que necesitan usar la clase. Los
comportamientos anteriores se denominan referencias activas a una clase . Además, todas las formas de hacer referencia a tipos, aunque hacen referencia a una clase, no activarán la inicialización de la clase, lo que se llama referencia pasiva . Por ejemplo: hacer referencia al campo estático de la clase principal a través de una subclase , hacer referencia a una subclase, pero no Hará que la subclase se inicialice; hacer referencia a la clase a través de la definición de matriz no activará la inicialización de esta clase, por ejemplo, User[] users = new User[10]no activará la inicialización de la clase Usuario; las constantes (modificación final estática) se almacenarán en el grupo constante de la clase que llama durante la fase de compilación. En esencia, no hay una referencia directa a la clase que define la constante, por lo que la inicialización de la clase que define la constante no se activará

Proceso de carga de clases

Todo el proceso de carga de clases , incluida la carga, verificación, preparación, análisis e inicialización.

carga

Durante la fase de carga, la máquina virtual debe completar tres cosas:

  1. Obtiene el flujo de bytes binarios que define una clase por su nombre completo
  2. Convierta la estructura de almacenamiento estática representada por este flujo de bytes 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 como punto de acceso para varios datos de esta clase en el área de método.

verificar

El propósito de la fase de verificación es garantizar que la información contenida en el flujo de bytes del archivo Class cumpla con todas las restricciones de la "Especificación de la máquina virtual Java" y garantizar que esta información no pondrá en peligro la seguridad de la máquina virtual. cuando se ejecuta como código. Debido a que el archivo de clase no
solo debe compilarse a partir del código fuente de Java. Se puede generar usando métodos que incluyen escribir directamente el archivo de clase en el editor binario usando el teclado 0 y 1. Si la JVM no verifica el flujo de bytes de entrada y confía completamente en él, es probable que cargue un flujo de bytecode con errores o intenciones maliciosas, provocando que todo el sistema sea atacado o incluso falle. Por lo tanto, verificar el código de bytes es una medida necesaria para JVM para protegerse. Si esta etapa es rigurosa determina directamente si la JVM puede resistir ataques
de código malicioso. En general, la etapa de verificación completará aproximadamente cuatro etapas de acciones de verificación: verificación de formato de archivo, verificación de metadatos, verificación de código de bytes y verificación de referencia de símbolos.

Preparar

La fase de preparación está oficialmenteVariables definidas en la clase.(Es decir, variables estáticas, variables modificadas por estáticas) La etapa de asignación de memoria y establecimiento del valor inicial de las variables de clase . En JDK 7 y anteriores, HotSpot utiliza la generación permanente para implementar el área de métodos, por lo que las variables de clase se almacenan en el área de métodos; en JDK 8 y posteriores, las variables de clase se almacenarán en el montón de Java junto con el objeto Class, por lo que " las variables de clase se almacenan en el área del método "Área" es una expresión completamente lógica.
En este momento, la asignación de memoria solo incluye variables de clase , 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. . En segundo lugar, el "valor inicial" mencionado aquí es "generalmente" el valor cero del tipo de datos . Por ejemplo, si una variable de clase se define como private static int v = 123;, entonces el valor inicial de la variable v después de la fase de preparación es 0 en lugar de 123, porque aún no ha comenzado Ejecute cualquier método Java, y la instrucción putstatic que asigna v a 123 se almacena en el método constructor de la clase <clinit>() después de compilar el programa, por lo que la acción de asignar v a 123 no se ejecutará hasta la fase de inicialización.
Como se mencionó anteriormente, el valor inicial es cero en "circunstancias normales". Si el atributo ConstantValue existe en la tabla de atributos de campo del campo de clase, entonces el valor de la variable se inicializará al valor inicial especificado por el Atributo ConstantValue durante la fase de preparación, es decir, si la variable de clase es una modificación final estática, se le asignará un valor específico, por ejemplo, private static final int v = 123;v se establecerá en 123 en la etapa de preparación.

analizar gramaticalmente

La fase de análisis es el proceso en el que la JVM reemplaza las referencias de símbolos en el grupo constante con referencias directas.

inicialización

La fase de inicialización es el último paso en el proceso de carga de clases. Entre las varias acciones de carga de clases mencionadas anteriormente, excepto que la aplicación del usuario puede participar parcialmente en la fase de carga a través de un cargador de clases personalizado , el resto de las acciones las realiza completamente Java. La máquina virtual toma el control. No es hasta la fase de inicialización que la JVM realmente comienza a ejecutar el código del programa Java escrito en la clase y transfiere el dominio a la aplicación.Planifique
inicializar variables de clase y otros recursos. Una declaración más directa es: la fase de inicialización es el proceso de ejecutar el método del constructor de clase <clinit>()

  1. El compilador (javac.exe) genera el método <clinit>() y recopila automáticamente todas las acciones de asignación de variables de clase en la clase y la declaración en el bloque de código estático . El orden de la colección del compilador está determinado por la declaración en el código fuente. archivo. El orden en que aparecen está determinado por el bloque de código estático. Solo se puede acceder a las variables definidas antes del bloque de código estático , y a las variables definidas después se les pueden asignar valores en el bloque de código estático anterior , pero no se puede acceder a ellas . (Es decir, solo se pueden escribir pero no leer. Debido a que puede haber otras declaraciones de asignación después del bloque de código estático, la lectura puede no ser el valor final de la variable cuando se ejecuta el bloque de código estático, por lo que la lectura no es permitido)
  2. El método <clinit>() es diferente del constructor de clase (método <init>()), no necesita llamar explícitamente al constructor de la clase principal, y la JVM se asegurará de que antes del método <clinit>() del Cuando se ejecuta la subclase, se ejecuta el método <clinit>() de la clase principal . Por lo tanto, la clase del primer método <clinit>() ejecutado en la JVM debe ser java.lang.Object
  3. El método <clinit>() no es necesario para una clase o interfaz. Si no hay un bloque de código estático ni una declaración de asignación a una variable en una clase, el compilador no necesita generar el método <clinit>() para esta clase.
  4. Los bloques de código estático no se pueden usar en la interfaz, pero todavía hay operaciones de asignación para la inicialización de variables, por lo que también se generará el método <clinit>(). Pero a diferencia de una clase, ejecutar el método <clinit>() de una interfaz no requiere ejecutar primero el método <clinit>() de la interfaz principal , porque la interfaz principal solo se inicializará cuando las variables definidas en la interfaz principal sean utilizado.
    Además, la clase de implementación de la interfaz no ejecutará el método <clinit>() de la interfaz durante la inicialización . Cuando se inicializa la subclase, la inicialización de la clase principal debe ocurrir antes de que se inicialice la subclase.
  5. La JVM debe garantizar que el método <clinit>() de una clase esté correctamente bloqueado y sincronizado en un entorno multiproceso . Si varios subprocesos inicializan una clase al mismo tiempo, solo uno de los subprocesos ejecutará <clinit>( ) método de esta clase.método, otros subprocesos deben bloquearse y esperar hasta que el subproceso activo termine de ejecutar el método <clinit>(). Por supuesto, una vez que el subproceso activo termina de ejecutar el método <clinit>(), otros subprocesos en espera no continuarán ejecutando el método <clinit>() después de ser despertados. Bajo el mismo cargador de clases, un tipo solo se inicializará una vez.

cargador de clases

El cargador de clases se refiere al código que implementa la acción de "obtener el flujo de bytes binarios que describe la clase a través del nombre completo de la clase" en la fase de carga de clases.

Clases y cargadores de clases.

Para cualquier clase, el cargador de clases que la carga y la clase misma (es decir, el nombre de clase completo) deben establecer conjuntamente su unicidad en la máquina virtual Java. Es decir, comparar si dos clases son "iguales" solo tiene sentido si las dos clases se cargan con el mismo cargador de clases; de lo contrario, incluso si las dos clases provienen del mismo archivo de clase y se cargan con la misma máquina virtual Java , Mientras los cargadores de clases que los cargan sean diferentes, las dos clases no deben ser iguales.

Modelo de delegación parental

Desde la perspectiva de la máquina virtual Java, solo hay dos cargadores de clases diferentes:
uno es el cargador de clases de arranque ( Bootstrap Class L oader Bootstrap\ Class\ LoaderB oo t s t r a p C a ss L o a d er   ), también conocido comocargador de clases bootstrap, este cargador de clases está implementado en lenguaje C++ y es parte de la propia máquina virtual. La instancia de este cargador de clases no puede ser accedido por el usuario Obtenido (si el método getParent () de un cargador de clases devuelve nulo, significa que su cargador de clases principal es el cargador de clases de arranque);

El otro son todos los demás cargadores de clases. Todos estos cargadores de clases están implementados en el lenguaje Java, todos existen fuera de la máquina virtual y todos heredan de la clase abstracta java.lang.ClassLoader. Primero, comprendamos la carga de clases proporcionada por lo siguiente tres sistemas
Dispositivo:

  • Inicie el cargador de clases : este cargador de clases es responsable de cargar las clases almacenadas en el directorio <JAVA_HOME>\lib (es decir, cargar la biblioteca de clases principal de Java, incluidas las clases que comienzan con los nombres de paquete java, javax y sun), o especificadas por el parámetro -Xbootclasspath La biblioteca de clases almacenada en la ruta y reconocida por la máquina virtual Java se carga en la memoria de la máquina virtual. Los programas Java no pueden hacer referencia directa al cargador de clases de inicio. Cuando los usuarios escriben un cargador de clases personalizado, si necesitan delegar la solicitud de carga al cargador de clases de inicio para su procesamiento, pueden usar directamente nulo en su lugar, es decir, usar el valor nulo. para representar la clase de arranque.cargador de clases
  • Cargador de clases de extensión ( Extensión del cargador de clases de extensión\ Clase\ CargadorExt e n s i o n Class Loa d er ) : este cargador de clases   se implementa como código Java en la clase sun.misc.Launcher$ ExtClassLoader . Responsable de cargar todas las bibliotecas de clases en el directorio <JAVA_HOME>\lib\ext, o en la ruta especificada por la variable del sistema java.ext.dirs. Este es en realidad un mecanismo de extensión para las bibliotecas de clases del sistema Java. El equipo de desarrollo de JDK permite a los usuarios colocar bibliotecas de clases universales en el directorio ext para ampliar las funciones de Java SE. Dado que el cargador de clases extendido se implementa en código Java, los desarrolladores pueden usarlo directamente en el programa para cargar archivos de clase.
  • Cargador de clases de aplicaciones ( Una aplicación Cargador de clases Aplicación\ Clase\ CargadorCargador de clases de aplicaciones ) : este cargador de clases se   implementa mediante sun.misc.Launcher $ AppClassLoader . Es el valor de retorno del método getSystemClassLoader() en la clase ClassLoader, por lo que también se le llama"cargador de clases del sistema". Es responsable de cargar la ruta de clases del usuario (ClassPath ClassPathClassPath , puede llamar a cualquier clase del proyecto paraxxx.class.getResource("/").toString() obtener todas las bibliotecas de clases en ClassPath ) . Los desarrolladores también pueden utilizar este cargador de clases directamente en el código. Si la aplicación no ha personalizado su propio cargador de clases, generalmente este es el cargador de clases predeterminado en el programa.

Las aplicaciones Java anteriores a JDK 9 se cargan mediante la cooperación de estos tres cargadores de clases. Los usuarios también pueden agregar cargadores de clases personalizados para expansión, como agregar fuentes de archivos de clases además de ubicaciones de disco, o mediante cargadores de clases implementar aislamiento de clases, sobrecarga y otras funciones. La relación de colaboración entre estos cargadores de clases es "generalmente" la siguiente (la razón de "generalmente" es porque, aunque el modelo de delegación principal se usó ampliamente después de la introducción de JDK 1.2, no es un modelo vinculante, sino una implementación del cargador de clases) . mejores prácticas recomendadas por los diseñadores de Java a los desarrolladores):
Modelo de delegación parental

Esta relación jerárquica entre cargadores de clases es el modelo de delegación principal de cargadores de clases ( Modelo de delegación de padres Padres\ Delegación\ ModeloModelo de delegación de padres de familia ) _ _ _ _ _ _ _ _ _ _ _ _   _

El modelo de delegación principal requiere que, además del cargador de clases de inicio de nivel superior, todos los demás cargadores de clases tengan su propio cargador de clases principal. La relación padre-hijo mencionada aquí generalmente no se implementa por herencia, sino mediante el uso de una relación combinada para reutilizar el código del cargador principal.

El flujo de trabajo es: si un cargador de clases recibe una solicitud de carga de clases, no intentará cargar la clase en sí primero, sino que delega la solicitud al cargador de clases principal para que la complete. Cada nivel del cargador de clases lo hará. Así es, todas las solicitudes de carga eventualmente debe enviarse al cargador de clases de inicio de nivel superior. Solo cuando el cargador principal informa que no puede completar la solicitud de carga (es decir, la clase requerida no se encuentra en su alcance de búsqueda), el subcargador intentará completar la carga. por sí mismo.

Bajo este modelo, un beneficio obvio es que las clases en Java tienen una relación jerárquica con prioridad junto con su cargador de clases , como java.lang.Object, que se almacena en rt.jar, sin importar qué cargador de clases quiera cargar esta clase. , eventualmente se delegará al cargador de clases de inicio en la parte superior del modelo para su carga. Por lo tanto, se puede garantizar que la clase Objeto sea la misma clase en varios entornos de cargador de clases del programa. :

  1. Evitando la carga repetida de clases, cuando el cargador principal ya ha cargado una clase, el cargador secundario no recargará la clase
  2. La seguridad está garantizada. Las clases en el directorio <JAVA_HOME>\lib solo serán cargadas por el cargador de clases de inicio. Imagínese si alguien define maliciosamente una clase con el mismo nombre que el directorio <JAVA_HOME>\lib, como java.lang.Integer, etc. si no existe un modelo de delegación parental, dicha clase se cargará e incluso se utilizará con éxito, entonces la API básica de Java se sobrescribirá y se alterará.

El código para implementar el modelo de delegación principal se concentra en el método loadClass() de java.lang.ClassLoader:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    
    
    synchronized (getClassLoadingLock(name)) {
    
    
        //首先检查这个类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
    
    
            long t0 = System.nanoTime();
            try {
    
    
            	//将请求委派给父类加载器
                if (parent != null) {
    
    
                    c = parent.loadClass(name, false);
                } else {
    
      //没有父类加载器,说明是启动类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
    
    
                //父类加载器找不到这个类就会抛出ClassNotFoundException,说明父类加载器无法完成加载请求
            }
            if (c == null) {
    
    
                long t1 = System.nanoTime();
                //父类加载器无法加载时,再调用自身的findClass()方法来进行加载
                c = findClass(name);
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
    
    
            resolveClass(c);
        }
        return c;
    }
}

La lógica del método es: primero verifique si el tipo solicitado a cargar se ha cargado. De lo contrario, llame al método loadClass () del cargador de clases principal. Si el cargador principal está vacío, el cargador de clases de inicio se utilizará como el cargador principal de forma predeterminada. Si el cargador de clases principal no puede cargar y genera una ClassNotFoundException, entonces llama a su propio método findClass() para intentar cargar.

Socavando el modelo de delegación de los padres

Como se puede ver en el contenido anterior, la implementación del modelo de delegación principal está en el método loadClass () de ClassLoader, por lo que para destruir el modelo de delegación principal, solo necesita definir un cargador de clases y reescribir el método loadClass ().

Tres momentos en la historia que rompieron el modelo de delegación de los padres

  1. En realidad , la primera vez ocurrió antes del modelo de delegación parental. Dado que el modelo de delegación principal solo apareció en JDK 1.2, el concepto de cargador de clases y la clase abstracta java.lang.ClassLoader ya existían en la primera versión de Java, lo que significa que alguien ya personalizó el cargador de clases y reescribió loadClass(). método. Para ser compatible con el código existente, no podemos evitar la posibilidad de que el método loadClass() sea sobrescrito por subclases. Solo podemos agregar un nuevo método findClass() para guiar a los usuarios a reescribir este método tanto como sea posible al escribir el suyo propio. Lógica de carga de clases, no el método loadClass(). Del análisis anterior del método loadClass(), podemos ver que si la clase principal no puede cargar la clase, la subclase misma llamará a su propio método findClass() para completar la carga, lo que no afectará la capacidad del usuario para cargar. la clase de acuerdo con su propia lógica, y Asegúrese de que el cargador de clases recién escrito se ajuste al modelo de delegación principal
  2. La segunda vez se debe a un defecto en el propio modelo. El modelo de delegación principal logra esto. Las clases más básicas son cargadas por el cargador de nivel superior. Si hay tipos básicos que necesitan ser llamados nuevamente al código del usuario (en ClassPath), el cargador de clases de inicio no podrá cargar a ellos. Por ejemplo, JDBC es un conjunto de especificaciones definidas por el propio Java. Definitivamente es un tipo muy básico en Java, pero necesita llamar al código SPI (Interfaz de proveedor de servicios) implementado por otros proveedores e implementado en la aplicación ClassPath, es decir, MySQL, Oracle y otras empresas Según la clase de controlador implementada por el propio JDBC, el cargador de clases que carga JDBC no debe reconocer estos códigos.
    Para solucionar este problema, Java introdujo el cargador de clases de contexto de subprocesos. Servicios similares a JDBC utilizan este subproceso. Cargador de clases de contexto para cargar El código de servicio SPI requerido, que es un cargador de clases principal para solicitar al cargador de clases secundario que complete el comportamiento de carga de clases, por lo que viola el modelo de delegación principal.
  3. La tercera vez se debió a la búsqueda de tecnologías de implementación en caliente, como el reemplazo en caliente de código y la implementación en caliente de módulos. Para algunos sistemas de producción, es muy importante implementar la implementación en caliente y actualizar sin reiniciar. Las implementaciones relevantes incluyen OSGi de IBM. La clave para lograr una implementación modular en caliente es la implementación de su mecanismo de cargador de clases personalizado. Muchas operaciones de búsqueda de clases se realizan en el cargador de clases plano, lo que destruye Aprenda las reglas del modelo de delegación principal

Tomcat rompe el modelo de delegación de padres

enlaces relacionados

Supongo que te gusta

Origin blog.csdn.net/Pacifica_/article/details/123647893
Recomendado
Clasificación