[Conocimiento profundo de JVM] 1. ¿Cómo cargar en la carga de clases de JVM? Los padres delegan la clase de carga y el modo mixto, demostración de código [requerido para la entrevista]

 

Orden de carga de JVM

 El compilador javac -> bytecode -> class loader -> memory. El paso de carga de la clase requiere tres pasos:

1. cargando (cargando)

  1. Obtenga el flujo de bytes binarios que define esta clase a través del nombre completo de una clase.
  2. Convierta la estructura de almacenamiento estática representada por este flujo de bytes en la estructura de datos en tiempo de ejecución del área de métodos.
  3. Un objeto java.lang.Class que representa esta clase se genera en el montón de Java como la entrada de acceso para estos datos en el área de métodos.
  4. Una vez completada la fase de carga, el flujo de bytes binarios se almacena en el área cuadrada de acuerdo con el formato requerido por la máquina virtual.

2. vinculación (asociación)

  1. Verificación (verificación): se usa para verificar si la clase cargada cumple con el estándar de análisis
    1. Verificación del formato de archivo: Verifique que la secuencia de bytes se ajuste a la especificación de formato de archivo de clase y que pueda ser procesada por la versión actual de la máquina virtual.
    2. Verificación de metadatos: análisis semántico de la información descrita por el código de bytes para garantizar que la información descrita cumpla con los requisitos de la especificación del lenguaje Java
    3. Verificación de código de bytes: esta etapa es principalmente para analizar el flujo de datos y el flujo de control para garantizar que los métodos de la clase verificada no realicen comportamientos que pongan en peligro la seguridad de la máquina virtual durante el tiempo de ejecución.
    4. Verificación de la referencia de símbolo: esta fase ocurre cuando la máquina virtual convierte la referencia de símbolo en una referencia directa (la fase de análisis), y principalmente verifica la coincidencia de información distinta de la clase en sí. El propósito es garantizar que la acción de análisis se pueda ejecutar con normalidad.
  2. preparación (preparación): Asignar valores por defecto a las variables estáticas de la clase Generalmente, el tipo básico es 0 y el tipo de referencia es nulo.
    1. Asignar formalmente memoria para variables y establecer valores iniciales. Estas memorias serán asignadas en el área de métodos. Las variables aquí solo incluyen escalares de clase y no incluyen variables de instancia.
  3. Resolución: convierta los símbolos de dirección en el grupo constante de la clase en direcciones de memoria directas y direcciones de memoria accesibles (no hay una asignación de inicialización real, muchas personas están acostumbradas a la confusión aquí)
    1. Referencia de símbolo: Una referencia de símbolo describe el objetivo referenciado con un conjunto de símbolos. El símbolo puede ser cualquier forma de literal, siempre que el objetivo se pueda ubicar sin ambigüedades cuando se utilice. La referencia simbólica no tiene nada que ver con el diseño de memoria implementado por la máquina virtual, y el destino al que se hace referencia no necesariamente ya está cargado en la memoria.
    2. Referencia directa: una referencia directa puede ser un puntero que apunta directamente al objetivo, un desplazamiento relativo o un identificador que puede localizar indirectamente el objetivo. El consumo directo de alcohol está relacionado con el diseño de la memoria.
    3. Resolución de clase o interfaz
    4. Análisis del campo
    5. Análisis del método de clase
    6. Análisis del método de interfaz

3. Inicialización (inicialización): El proceso de llamar al método <clinit> del constructor de clases, asignar valores iniciales a las variables miembro estáticas, y luego comienza la asignación de inicialización real . (La reordenación de instrucciones entre 2.2 y 2.2 también es la razón de la semi-instanciación. Puede usar el complemento BinEd-Binary para ver el archivo de código de bytes compilado. Esta es también la razón principal de la carga diferida en modo singleton para usar volátiles )

Cargador de clases

1. El cargador de clases Bootstrap (cargador de clases de inicio) es el cargador de clases de nivel superior. La ruta de carga es el archivo jar / charset.jar y otras clases principales en el directorio < JAVA_HOME > \ lib, implementado en C ++.
2. El cargador de clases de extensión (cargador de clases de extensión) es responsable de cargar los archivos jar en el directorio < JAVA_HOME > \ lib \ ext o especificado por -Djava.ext.dirs.
3. El cargador de clases de la aplicación (cargador de clases del sistema ) es responsable de cargar el paquete de clases JAR y la ruta de clases especificada por la ruta de clase o la propiedad del sistema java.class.path o la propiedad del sistema operativo CLASSPATH en el comando java. en la aplicación Defina su propio cargador de clases, en circunstancias normales, este es el cargador de clases predeterminado en el programa.
4. Personalizado es (cargador de clases personalizado) , que es implementado por el usuario.

Diagrama del proceso de delegación parental

Mecanismo de delegación de padres.

Inserte la descripción de la imagen aquí

Primero usamos código para demostrar la existencia de delegación parental. (Puede verificarlo usted mismo)

Agrega una pregunta aquí:

¿Cómo se especifica el padre?

El código fuente se especifica con super (padre).

Además, si no se especifica, el valor predeterminado es AppClassLoader @ 18b4aac2. Puede probarlo con el código usted mismo. No encontré el valor predeterminado para el código fuente.

public class T004_ParentAndChild {
    public static void main(String[] args) {
        System.out.println(T004_ParentAndChild.class.getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getClass().getClassLoader());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent());
        System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent());
        //System.out.println(T004_ParentAndChild.class.getClassLoader().getParent().getParent().getParent());

    }
}

打印出来是这样:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
sun.misc.Launcher$ExtClassLoader@12bb4df8
null

(El proceso de carga de ClassLoader utiliza el patrón de diseño de patrón de método de plantilla)

La JVM se carga dinámicamente a pedido, utilizando el mecanismo de delegación principal , de abajo hacia arriba para verificar si la clase está cargada (el orden de 1> 2> 3 en la figura), si ningún cargador de clases ha cargado la clase, estará arriba -down (4> 5)> 6) Busque la clase cargada nuevamente, hasta que se encuentre la clase y se cargue en la memoria.

La ventaja de usar el mecanismo de delegación padre es por seguridad , para evitar conflictos entre la clase cargada externamente y la cadena del cargador de clases interno, y daños malintencionados. Si se carga una clase con el mismo nombre, la JVM primero determinará si dicha clase tiene Si ya se ha cargado, la clase no se volverá a cargar.

Puede ver el código fuente de ClassLoader: el
parámetro de nombre es el nombre de la clase y la llamada real es loadClass (nombre de cadena)

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

调用 loadClass (nombre de cadena, resolución booleana)

La secuencia de ejecución es como se muestra en la figura anterior:

  1. Primero llame a findLoadedClass (nombre) para ver si la misma clase se ha cargado antes. ¿En cuanto a dónde encontrarla? Escuché que primero vaya a la tabla hashset en la memoria para buscar (no encontré el código fuente, puede hablar sobre él si lo encuentra).
  2. Si no puede encontrarlo, llame a parent.loadClass (); aquí hay una iteración. Hasta que se encuentre.
  3. Si no se puede encontrar, llamará a findClass (nombre); el método arroja ClassNotFoundException (nombre).
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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 thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    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;
        }
    }

¿Por qué la JVM usa la delegación parental?

  1. Por seguridad (lo más importante)
    1. Si no necesita la delegación de los padres, cargue el cargador personalizado en java.lang.String y luego empaquételo y envíelo al cliente, sobrescribirá la biblioteca de cadenas incorporada. Generalmente, guardamos las contraseñas en el almacenamiento de cadenas. En este momento, si agrego una línea comercial de envío de buzones de correo o almacenamiento en la base de datos de mi objeto en la clase de cadena personalizada, ¿es equivalente a usar mi cadena personalizada? Obtendré fácilmente la contraseña.
  2. Por la eficiencia

Expansión:

1. Cifrado del cargador de clases

Se puede cifrar cuando se carga el cargador de clases, el código es el siguiente:

El que se usa aquí también puede estar encriptado.

public class T007_MSBClassLoaderWithEncription extends ClassLoader {

    public static int seed = 0B10110110;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("c:/test/", name.replace('.', '/').concat(".msbclass"));

        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;

            while ((b=fis.read()) !=0) {
                baos.write(b ^ seed);
            }

            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();//可以写的更加严谨

            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name); //throws ClassNotFoundException
    }

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

        encFile("com.mashibing.jvm.hello");

        ClassLoader l = new T007_MSBClassLoaderWithEncription();
        Class clazz = l.loadClass("com.mashibing.jvm.Hello");
        Hello h = (Hello)clazz.newInstance();
        h.m();

        System.out.println(l.getClass().getClassLoader());
        System.out.println(l.getParent());
    }

    /**
     * 用亦或加密
     * @param name
     * @throws Exception
     */
    private static void encFile(String name) throws Exception {
        File f = new File("c:/test/", name.replace('.', '/').concat(".class"));
        FileInputStream fis = new FileInputStream(f);
        FileOutputStream fos = new FileOutputStream(new File("c:/test/", name.replaceAll(".", "/").concat(".msbclass")));
        int b = 0;

        while((b = fis.read()) != -1) {
            // 亦或一个数,再亦或的话就解密了
            fos.write(b ^ seed);
        }

        fis.close();
        fos.close();
    }
}

2. ¿Cuándo comienza a inicializarse el cargador de clases?

La especificación JVM no especifica cuándo cargar. Pero estipula estrictamente cuándo debe inicializarse.

La carga de clases no es para cargar todas las clases durante la inicialización, sino para cargar las clases cuando se utilizan. La carga diferida se producirá. Hay cinco situaciones para la carga diferida:

  1. Nuevo objeto, obtenga y acceda a variables estáticas, recuerde acceder a las variables finales excepto.

  2. Cuando java.lang.reflect realiza una llamada de reflexión a una clase.

  3. Al inicializar una subclase, la clase principal se inicializa primero.

  4. Cuando se inicia la máquina virtual, se debe inicializar la clase principal a ejecutar.

  5. Cuando el soporte de lenguaje dinámico java.lang.invoke.MethodHandle se resuelve en el identificador de método de REF_getstatic REF_putstatic REF_invokestatic, la clase debe inicializarse.

3. ¿Cómo romper el mecanismo de delegación de los padres?

Podemos inferir del código fuente anterior que siempre que se anule el método loadClass () de ClassLoader, no se llama a loadClass () de la clase padre en el método loadClass, sino directamente para cargar su propia clase, de modo que el padre El mecanismo de delegación puede romperse .

4. ¿Cuándo se romperá el mecanismo de delegación de los padres?

  1. Antes de la versión JDK1.2, el ClassLoader personalizado personalizado debe anular loadClass ().
  2. ThreadContextClassLoader puede implementar el código de clase de implementación de llamada de clase básica, que es especificado por thread.setContextClassLoader.
  3. Arranque en caliente. osgi tomcat tiene su propio módulo designado como cargador de clases (se pueden cargar diferentes versiones de la misma biblioteca de clases)
    1. La implementación en caliente de tomcate rompe el mecanismo de delegación principal. La modificación de un archivo de clase se puede sincronizar inmediatamente con el contexto. De hecho, la esencia es volver a cargar la clase una vez. Si está interesado, puede aprender sobre el principio de implementación de tomcat. Escribió el método loadClass ().

Reescribe el código loadClass ():

public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            File f = new File("C:/work/ijprojects/JVM/out/production/JVM/" + name.replace(".", "/").concat(".class"));

            if(!f.exists()) return super.loadClass(name);

            try {

                InputStream is = new FileInputStream(f);

                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }

            return super.loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        MyLoader m = new MyLoader();
        Class clazz = m.loadClass("com.mashibing.jvm.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazzNew);
    }
}

5. Modo mixto de JVM

 

Por defecto, es un modo mixto (intérprete mixto + compilación de código activo).

Java se interpreta y ejecuta, una vez que el archivo de clase está en la memoria, se ejecuta a través del intérprete de código de bytes de Java.

JIT (compilador Just In-Time): algunos códigos se compilarán en un código de formato local para su ejecución.

Por lo tanto, no se puede decir simplemente que Java sea un lenguaje interpretado o un lenguaje compilado.

1. ¿Cuándo se utilizará JIT para compilar código local?

    Escribí un fragmento de código y se ejecutó con un intérprete al principio. Resultó que cierto fragmento de código se ejecutó con mucha frecuencia durante el proceso de ejecución (ejecutado cientos de miles de veces en 1s), y la JVM compile este fragmento de código en código local (similar a usar el lenguaje C para compilar el archivo * .exe local), cuando el código se ejecuta nuevamente, el intérprete no se utilizará para ejecutarlo, lo que mejora la eficiencia.

2. ¿Por qué no compilar directamente código local para mejorar la eficiencia de ejecución?

  1. La eficiencia de ejecución del intérprete de Java es realmente muy alta, y la eficiencia de ejecución de algunos códigos no se pierde necesariamente con la ejecución del código nativo.
  2. Si el código ejecutado hace referencia a muchas bibliotecas de clases, el tiempo de ejecución será muy largo.

Tres, use parámetros para cambiar el modo:

  1. -Xmixed: el modo predeterminado es mixto, la velocidad de inicio es más rápida y el código activo se detecta y compila.
  2. -Xint: use el modo de interpretación, inicie rápidamente y ejecute un poco más lento.
  3. -Xcomp: usa el modo de compilación puro, ejecución rápida, inicio lento (cuando hay muchas bibliotecas)

Verificación de código:

Editar configuraciones -> Opciones de VM

  • Modo mezclado:
    • No modifique ningún parámetro y use la configuración predeterminada
    • Tiempo de ejecución: alrededor de 2700
  • Modo de explicación:
    • 将 Editar configuraciones -> Opciones de VM -> - Xint
    • Tiempo de ejecución: el tiempo de ejecución es demasiado largo, menos un ciclo de 0,19000
  • Modo de compilación:
    • 将 Editar configuraciones -> Opciones de VM -> - Xcomp
    • Tiempo de ejecución: 2600
public class T009_WayToRun {
    public static void main(String[] args) {
        for(int i=0; i<10_0000; i++)
            m();

        long start = System.currentTimeMillis();
        for(int i=0; i<10_0000; i++) {
            m();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    public static void m() {
        for(long i=0; i<10_0000L; i++) {
            long j = i%3;
        }
    }
}

Artículo siguiente: [Conocimiento profundo de JVM] 3. Almacenamiento de CPU + MESI + pseudo-intercambio de CPU + Problema de desorden de CPU y demostración de código [entrevista esencial]

Supongo que te gusta

Origin blog.csdn.net/zw764987243/article/details/109502435
Recomendado
Clasificación