Mecanismo de carga de clases de Java de superficial a profundo

Mecanismo de carga de clases de Java de superficial a profundo

(1) Breve descripción

La máquina virtual Java carga los datos que describen la clase desde el archivo .class en la memoria y realiza la verificación, conversión, análisis e inicialización de los datos, y finalmente forma un tipo Java que puede ser utilizado directamente por la máquina virtual. Este es el proceso de carga de clases de la máquina virtual Java.

La carga de clases se refiere específicamente a leer los datos binarios en el archivo .class generado después de que el código se compila en la memoria, colocarlo en el área de método del área de datos en tiempo de ejecución (implementado como un metaespacio) y luego crear un java en el área del montón El objeto .lang.Class se utiliza para encapsular la estructura de datos de la clase en el área de métodos. Entonces, podemos pensar que el producto final de la carga de clases es el objeto Class ubicado en el área del montón. El objeto Class proporciona a los programadores Java una interfaz para acceder a la estructura de datos en el área de métodos.

La siguiente figura muestra el flujo básico del mecanismo de carga de clases, que también es el ciclo de vida completo de una clase desde la creación hasta la destrucción:

Inserte la descripción de la imagen aquí
Se determina el orden de las cinco etapas de carga, verificación, preparación, inicialización y descarga. El proceso de carga de la clase debe iniciarse paso a paso en este orden, mientras que la fase de análisis no es necesariamente, puede ser posterior a la fase de inicialización en algunos casos Para empezar de nuevo, esto es para admitir el enlace en tiempo de ejecución del lenguaje Java (también conocido como enlace dinámico o enlace tardío). Comencemos con un análisis detallado del proceso de carga de clases:

(2) Proceso de carga de clases

1. Carga

Encontrar y cargar los datos binarios de una clase es la primera etapa del proceso de carga de la clase. En la fase de carga, la máquina virtual debe completar las siguientes tres cosas:

  1. Obtenga el flujo de bytes binarios definido por una clase por su nombre completo.
  2. La estructura de almacenamiento estática representada por este flujo de bytes se transforma 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 a los datos en el área de métodos.

En comparación con otras etapas de carga de clases, la etapa de carga (para ser precisos, la acción de obtener el flujo de bytes binarios de la clase) es la etapa más controlable, porque los desarrolladores pueden usar el cargador de clases proporcionado por el sistema. Para completar la carga, también puede personalizar su propio cargador de clases para completar la carga.

Una vez que se completa la fase de carga, el flujo de bytes binarios fuera de la máquina virtual se almacena en el área de métodos en el formato requerido por la máquina virtual, y también se crea un objeto de la clase java.lang.Class en el montón de Java, de modo que el Estos datos en el área de método de acceso a objetos.

Parte del contenido de la fase de carga y la fase de vinculación (como parte de las acciones de verificación del formato de archivo de código de bytes) se llevan a cabo alternativamente. La fase de carga no se ha completado y la fase de vinculación puede haber comenzado, pero estas acciones se llevan a cabo durante la fase de carga. Aún pertenece al contenido de la etapa de conexión y la hora de inicio de las dos etapas aún mantiene una secuencia fija.

2. Verificación

Una vez que la JVM ha cargado el archivo de código de bytes de clase y ha creado el objeto de clase correspondiente en el área de métodos, la JVM iniciará la verificación del flujo de código de bytes. Sólo los archivos que cumplen con la especificación de código de bytes de la JVM pueden ser ejecutados correctamente por la JVM. La verificación de bytecode incluye principalmente los siguientes elementos:

  • Verificación del formato de archivo: por ejemplo, si hay constantes no admitidas en las constantes, si hay información irregular o adicional en el archivo.
  • Verificación de metadatos: por ejemplo, si la clase hereda la clase modificada final, si los campos y métodos de la clase entran en conflicto con la clase principal y si hay una sobrecarga no razonable.
  • Verificación de código de bytes: para garantizar la racionalidad de la semántica del programa, por ejemplo, para garantizar la racionalidad de la conversión de tipos.
  • Verificación de la referencia de símbolo: Por ejemplo, compruebe si la clase correspondiente se puede encontrar a través del nombre completo en la referencia de símbolo y si la clase actual puede acceder a la accesibilidad (privada, pública, etc.) en la referencia de símbolo.

3. Preparación

La etapa de preparación es la etapa de asignación formal de memoria para la variable de clase y establecimiento del valor inicial de la variable de clase, estas memorias serán asignadas en el área de métodos.

Debemos tener en cuenta que hay dos tipos de variables en Java: variables de clase y variables de miembro de clase.Las variables de clase se refieren a variables modificadas por estática, mientras que todos los demás tipos de variables pertenecen a variables de miembro de clase. En la fase de preparación, JVM solo asignará memoria para variables de clase, pero no para variables de miembro de clase. La asignación de memoria de las variables miembro de la clase no comienza hasta la fase de inicialización.

En la fase de preparación, JVM asignará memoria para las variables de clase y las inicializará. Pero la inicialización aquí se refiere a asignar el valor cero del tipo de datos en el lenguaje Java a la variable, no el valor inicializado en el código de usuario. Pero si una variable es constante (modificada por estática final), entonces en la fase de preparación se le dará al atributo el valor que el usuario desea.

4. Análisis

La etapa de análisis es un proceso en el que la máquina virtual reemplaza las referencias de símbolo en el grupo constante con referencias directas. Las acciones de análisis se realizan principalmente en los 7 tipos de referencias de símbolo de clases o interfaces, campos, métodos de clase, métodos de interfaz, tipos de métodos, identificadores de métodos y calificadores de puntos de llamada. Introduzcamos brevemente referencias simbólicas y referencias directas:

  • Referencia de símbolo: una cadena, pero esta cadena proporciona cierta información que puede identificar de forma única un método, una variable y una clase.
  • Referencia directa: puede entenderse como una dirección de memoria o un desplazamiento. Por ejemplo, para los métodos de clase, las referencias directas a las variables de clase son indicadores del área de método. Por ejemplo, los métodos, la referencia directa de una variable de instancia es el desplazamiento desde el puntero principal de la instancia hasta la posición de la variable de instancia.

Por ejemplo, ahora llame al método hello (), la dirección de este método es 1234567, hola es una referencia simbólica y 1234567 es una referencia directa. En la fase de análisis, la máquina virtual reemplaza todas las referencias simbólicas como nombres de clases, nombres de métodos y nombres de campos con direcciones de memoria específicas o compensaciones, es decir, referencias directas. Esta parte es básicamente transparente para los programadores, siempre que la entendamos.

5. Inicialización

Al introducir la inicialización, primero debemos introducir dos métodos: <clinit> y <init>. Estos dos métodos se generan automáticamente al compilar y generar el archivo .class, uno es el método de inicialización de la clase y el otro es el método de inicialización de la instancia.

  • <clinit>: se llama cuando la JVM carga el archivo de clase por primera vez, incluida la ejecución de sentencias de inicialización de variables estáticas y bloques estáticos.
  • <init>: se llama cuando se crea la instancia, incluida la llamada al nuevo operador, la llamada al método newInstance () del objeto Class o Java.lang.reflect.Constructor, la llamada al método clone () de cualquier objeto existente y el uso de java.io El método getObject () de la clase .ObjectInputStream deserializa.

El método <clinit> () es generado por el compilador que recopila automáticamente la acción de asignación de todas las variables de clase en la clase y la declaración en el bloque de declaraciones estáticas static {}. El orden de la colección del compilador es el orden en el que aparecen las declaraciones en el archivo fuente Determinado, el bloque de declaraciones estáticas solo puede acceder a las variables definidas antes del bloque de declaraciones estáticas. Para las variables definidas después del bloque de instrucciones estáticas, el bloque de instrucciones estáticas se puede asignar pero no se puede acceder a ellas.

La fase de inicialización de la clase comienza a asignar valores iniciales correctos a las variables estáticas de la clase, y la JVM se encarga de inicializar la clase, principalmente para inicializar las variables de la clase. En otras palabras, solo inicialice variables o declaraciones modificadas por estática (esta oración es el punto). Hay dos formas de inicializar variables de clase en Java:

  • El valor inicial especificado cuando se declara la variable de clase.
  • Use bloques de código estático para especificar valores iniciales para variables de clase

En la etapa de inicialización, el código del programa Java definido por el usuario realmente comienza a ejecutarse. En esta etapa, la JVM inicializará el objeto de clase de acuerdo con el orden de ejecución de la declaración. En términos generales, la inicialización se activará cuando la JVM se encuentre con las siguientes 5 situaciones:

  • Al encontrar las cuatro instrucciones de código de bytes de new, getstatic, putstatic e invokestatic, si la clase no se ha inicializado, su inicialización debe activarse primero. El escenario de código Java más común para generar estas 4 instrucciones es: cuando se usa la nueva palabra clave para crear una instancia de un objeto, leer o configurar un campo estático de una clase (modificado por final, el resultado se ha puesto en el grupo estático en tiempo de compilación Field excepto) y al llamar a un método estático de una clase.
  • Cuando se utiliza el método del paquete java.lang.reflect para realizar una llamada de reflexión a una clase, si la clase no se ha inicializado, primero se debe activar su inicialización.
  • Al inicializar una clase, si descubre que su clase principal no se ha inicializado, primero debe activar la inicialización de su clase principal.
  • Cuando se inicia la máquina virtual, el usuario necesita especificar una clase principal que se ejecutará (la clase que contiene el método main ()), y la máquina virtual inicializa esta clase principal primero.
  • Cuando se utiliza el soporte de lenguaje dinámico JDK1.7, si el resultado del análisis final de una instancia de java.lang.invoke.MethodHandle es el identificador de método de REF_getstatic, REF_putstatic y REF_invokeStatic, y la clase correspondiente a este identificador de método no está inicializada, primero debe activarse Su inicialización.

6. Utilice

Una vez que la JVM completa la fase de inicialización de la clase, la JVM comienza a ejecutar el código de programa del usuario desde el método de entrada.

7. Desinstalar

Cuando se ejecuta el código del programa de usuario, la JVM comienza a destruir el objeto Class creado y, finalmente, la JVM responsable de la ejecución también sale de la memoria.

Podemos resumir el orden de ejecución de la carga de clases:

  1. Determine el valor inicial de la variable de clase: en la fase de preparación de la carga de clases, la JVM inicializará el valor cero para la variable de clase y luego la variable de clase tendrá un valor inicial de cero. Si es una variable de clase modificada por final, se inicializará directamente al valor que desee el usuario.
  2. Método de entrada de inicialización: al entrar en la fase de inicialización de la carga de clases, la JVM buscará la entrada completa del método principal para inicializar toda la clase donde se encuentra el método principal. Cuando se necesita inicializar una clase, primero se inicializa el constructor de la clase.
  3. Inicialice el constructor de clase: JVM recopilará las sentencias de asignación y los bloques de código estático de las variables de clase en orden, y finalmente formará el constructor de clase que será ejecutado por la JVM.
  4. Inicialice el constructor de objetos: JVM recopilará las declaraciones de asignación de variables miembro, bloques de código ordinarios y, finalmente, recopilará los métodos de construcción, los compondrá en el constructor de objetos y finalmente los ejecutará mediante la JVM.

Si encuentra la inicialización de otras clases al inicializar la clase donde se encuentra el método principal, cargue primero la clase correspondiente y regrese después de que se complete la carga. Repita este ciclo y finalmente regrese a la clase del método principal.

(3) Modelo de delegación de padres y cargador de clases

1. Cargador de clases

Para cualquier clase, necesita cargar su cargador de clases y la clase en sí para determinar la unicidad de esta clase en la máquina virtual Java.Cada cargador de clases tiene un espacio de nombre de clase independiente. En otras palabras, si compara si dos clases son la misma clase, además de comparar si los nombres completos de las dos clases son iguales, también debe comparar si las dos clases están cargadas por el mismo cargador de clases. Incluso si el mismo archivo de clase se carga en la misma máquina virtual dos veces, si lo cargan dos cargadores de clases diferentes, las dos clases siguen sin ser la misma clase. Esta comparación de igualdad afectará a algunos métodos, como el método equals (), el método isAssignableFrom (), el método isInstance (), etc. del objeto Class, así como la palabra clave instanceof para determinar la propiedad del objeto.

Hay tres tipos de cargadores para JVM, a saber:

  • Cargador de clases de arranque: el cargador de clases de arranque es un cargador de clases implementado en C ++, que es responsable de cargar la biblioteca de clases principal en JAVA_HOME / lib o el paquete jar especificado por la opción -Xbootclasspath a la biblioteca de clases reconocida por la máquina virtual En memoria. Dado que el cargador de clases de inicio incluye los detalles de implementación local de la máquina virtual, los desarrolladores no pueden obtener directamente una referencia al cargador de clases de inicio.
  • Cargador de clases de extensión: El cargador de clases de extensión es implementado por ExtClassLoader de Sun, que es responsable de cargar en la memoria la biblioteca de clases en JAVA_HOME / lib / ext o la ubicación especificada por la variable de sistema -Djava.ext.dir. Los desarrolladores pueden usar directamente el cargador de clases extendido estándar.
  • Cargador de clases del sistema (System): El cargador de clases del sistema es implementado por AppClassLoader de Sun, que es responsable de cambiar la ruta de clases del usuario (java -classpath o -Djava.class.path) al directorio al que apunta la variable, es decir, la ruta de clases actual y su La biblioteca de clases bajo la ruta de la biblioteca de clases de terceros referenciada) se carga en la memoria. Los desarrolladores pueden utilizar directamente el cargador de clases del sistema.
    Inserte la descripción de la imagen aquí
    En la figura anterior, podemos ver claramente la relación padre-hijo de los cargadores de clases propios de las tres JVM, pero la relación padre-hijo aquí se realiza por combinación, en lugar de herencia. El siguiente diagrama de herencia puede probar esta afirmación:

Inserte la descripción de la imagen aquí
Obviamente, tanto ExtClassLoader como AppClassLoader heredan de URLClassLoader, por lo que no existe una relación de herencia entre los dos. Podemos ver los detalles específicos de estos tres tipos de cargadores desde el punto de vista de la fuente para entender por qué estos tres cargadores de clases son superficialmente La relación de herencia en realidad no se realiza mediante la relación de herencia.

Podemos escribir un fragmento de código para verificar esto:

class T{
    
    
    static int m = 0;
}
public class Test {
    
    
    public static void main(String[] args) {
    
    
        T t = new T();
        ClassLoader classLoader1 = t.getClass().getClassLoader();
        System.out.println(classLoader1);
        ClassLoader classLoader2 = t.getClass().getClassLoader().getParent();
        System.out.println(classLoader2);
        ClassLoader classLoader3 = t.getClass().getClassLoader().getParent().getParent();
        System.out.println(classLoader3);
    }
}

Este código puede permitirnos entender que el llamado cargador de clases padre es en realidad una propiedad padre en el objeto classLoader. Es una referencia al cargador de clases padre. A través de él, podemos encontrar la carga padre de cualquier cargador de clases. Aparentemente, la salida del código debería ser la siguiente:

jdk.internal.loader.ClassLoaders$AppClassLoader@3d4eac69
jdk.internal.loader.ClassLoaders$PlatformClassLoader@77459877
null

Nota: El PlatformClassLoader aquí es en realidad ExtClassLoader. Después de Java9, el ExtClassLoader original se ha cambiado a PlatformClassLoader con la misma función.

Algunas personas pueden tener curiosidad por saber por qué la última salida es nula, la razón es muy simple, porque el cargador de clases principal de ExtClassLoader es BootStrapClassLoader, pero está implementado por el método nativo (C ++), por lo que no podemos encontrar su referencia. A continuación, podemos ver este atributo principal del código fuente:

public abstract class ClassLoader {
    
    
	// parent属性在这里
	private final ClassLoader parent;
	
	private static ClassLoader scl;
	
	private ClassLoader(Void unused, ClassLoader parent) {
    
    
	    this.parent = parent;
	    ...
	}
	protected ClassLoader(ClassLoader parent) {
    
    
	    this(checkCreateClassLoader(), parent);
	}
	
	protected ClassLoader() {
    
    
	    this(checkCreateClassLoader(), getSystemClassLoader());
	}
	
	public final ClassLoader getParent() {
    
    
	    if (parent == null)
	        return null;
	    return parent;
	}
	
	public static ClassLoader getSystemClassLoader() {
    
    
	    initSystemClassLoader();
	    if (scl == null) {
    
    
	        return null;
	    }
	    return scl;
	}
	
	private static synchronized void initSystemClassLoader() {
    
    
	    if (!sclSet) {
    
    
	        if (scl != null)
	            throw new IllegalStateException("recursive invocation");
	        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
	        if (l != null) {
    
    
	            Throwable oops = null;
	            //通过Launcher获取ClassLoader
	            scl = l.getClassLoader();
	            try {
    
    
	                scl = AccessController.doPrivileged(
	                    new SystemClassLoaderAction(scl));
	            } catch (PrivilegedActionException pae) {
    
    
	                oops = pae.getCause();
	                if (oops instanceof InvocationTargetException) {
    
    
	                    oops = oops.getCause();
	                }
	            }
	            if (oops != null) {
    
    
	                if (oops instanceof Error) {
    
    
	                    throw (Error) oops;
	                } else {
    
    
	                    // wrap the exception
	                    throw new Error(oops);
	                }
	            }
	        }
	        sclSet = true;
	    }
	}
}

Podemos ver que getParent () en realidad devuelve un padre del objeto ClassLoader, y la asignación del padre está en el constructor del objeto ClassLoader. Tiene dos situaciones:

  1. Al crear un ClassLoader por una clase externa, especifique directamente un ClassLoader como padre.
  2. Generado por el método getSystemClassLoader (), que se obtiene mediante getClassLoader () en sun.misc.Laucher, que es AppClassLoader.

Para decirlo sin rodeos, si se crea un ClassLoader sin especificar un padre, su padre es AppClassLoader de forma predeterminada.

Ahora que hemos descubierto el atributo padre, ¿dónde está la fuente de este atributo? Para todo el problema, veamos primero la clase Launcher, en la que se crearán el constructor de la clase de extensión y el constructor de la clase del sistema:

public class Launcher {
    
    
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
    
    
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
    
    
        // 创建扩展类加载器
        ClassLoader extcl;
        try {
    
    
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
    
    
            throw new InternalError("Could not create extension class loader", e);
        }

        // 创建系统类加载器
        try {
    
    
	    // 将ExtClassLoader对象实例传递进去
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
    
    
            throw new InternalError("Could not create application class loader", e);
	}

	public ClassLoader getClassLoader() {
    
    
        return loader;
    }
    
	static class ExtClassLoader extends URLClassLoader {
    
    
        public static ExtClassLoader getExtClassLoader() throws IOException{
    
    
            final File[] dirs = getExtDirs();
            try {
    
    
                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
    
    
                        public ExtClassLoader run() throws IOException {
    
    
                            // ExtClassLoader在这里创建
                            return new ExtClassLoader(dirs);
                        }
            	});
            } catch (java.security.PrivilegedActionException e) {
    
    
                throw (IOException) e.getException();
    		}
		}

       	public ExtClassLoader(File[] dirs) throws IOException {
    
    
           	super(getExtURLs(dirs), null, factory);
    	}
	}
}

Podemos prestar atención a las siguientes tres líneas de código:

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

Encontraremos que la instancia de ExtClassLoader se pasa al método getAppClassLoader, por lo que se debe haber asignado un valor al objeto principal de AppClassLoader, pero parece que ExtClassLoader no asigna directamente un valor a la propiedad principal. De hecho, su constructor llama al constructor de su clase padre, URLClassLoder, y pasa 3 parámetros:

public ExtClassLoader(File[] dirs) throws IOException {
    
    
	super(getExtURLs(dirs), null, factory);   
}

El método de construcción URLClassLoader correspondiente:

public  URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
    
    
     super(parent);
}

Desde aquí podemos entender por qué el cargador de clases padre de ExtClassLoader es nulo.

Además de los tres cargadores de clases originales, también podemos personalizar los cargadores de clases. Antes de hablar sobre los cargadores de clases personalizados, hablemos de una de las especificaciones más importantes de los cargadores de clases: el modelo de delegación principal.

2. Modelo de delegación de padres

JVM utiliza el mecanismo de delegación padre de forma predeterminada al cargar clases. En términos sencillos, cuando un cargador de clases en particular recibe una solicitud para cargar una clase, primero delega la tarea de carga al cargador de clases principal y luego de forma recursiva (esencialmente la llamada recursiva de la función loadClass), por lo que todas las solicitudes de carga Finalmente, deberían transferirse al cargador de clases de inicio de nivel superior. Si el cargador de clases principal puede completar la solicitud de carga de clases, regresa correctamente. Solo cuando el cargador de clases principal no puede completar la solicitud de carga, el cargador secundario intentará cargarlo por sí mismo. De hecho, en la mayoría de los casos, las clases más básicas las carga el cargador de nivel superior, porque estas clases básicas se llaman "básicas" porque siempre se utilizan como API llamadas por código de usuario (por supuesto, también hay El caso en el que la clase básica vuelve a llamar al código de usuario del usuario, es decir, el caso en el que el modelo de delegación padre está roto).

Inserte la descripción de la imagen aquí
Entonces, ¿por qué usar este mecanismo? Debido a que esto puede evitar la carga repetida, cuando el padre ya ha cargado la clase, no es necesario volver a cargarla. Teniendo en cuenta el factor de seguridad, imaginemos que si no usamos este modo de delegación, podemos usar una cadena personalizada para reemplazar dinámicamente el tipo definido en la API del núcleo de Java en cualquier momento. Esto tendrá un riesgo de seguridad muy grande, y la delegación principal Esto se puede evitar porque el cargador de clases Bootstrap ya carga String al inicio, por lo que un ClassLoader definido por el usuario nunca puede cargar un String escrito por usted mismo a menos que cambie el ClassLoader en el JDK El algoritmo predeterminado para buscar clases.

El siguiente diagrama de secuencia muestra todo el proceso de forma muy clara:

Inserte la descripción de la imagen aquí
Una cosa que hay que tener claro es que el modelo de delegación de los padres es solo una recomendación oficial, no todas las regulaciones que se deben seguir. Debido a que la extensión de este artículo es demasiado larga, hablaré sobre los posibles problemas con el modelo de delegación principal y cómo personalizar el cargador de clases en el próximo artículo.

(4) Espacio de nombres (NameSpace)

Los amigos que estén familiarizados con C ++ deberían estar familiarizados con esto. De hecho, la función del espacio de nombres en Java es similar a la de C ++. Cuando JVM determina si dos clases son iguales, no solo determina si los nombres de paquete de las dos clases son iguales, sino también si la clase es cargada por la misma instancia del cargador de clases. Solo si ambos se satisfacen al mismo tiempo, la JVM considerará que las dos clases son iguales.

Las clases en el mismo espacio de nombres son mutuamente visibles, y el espacio de nombres consta de las clases cargadas por el cargador y todo su cargador principal. En el mismo espacio de nombres, no aparecerán dos clases con el mismo nombre completo (incluido el nombre del paquete de la clase). En diferentes espacios de nombres, el nombre completo de la clase (incluido el nombre del paquete de la clase) puede ser el mismo De las dos clases.

El párrafo anterior puede ser un poco abstracto, primero tomemos un ejemplo. La clase cargada por el cargador secundario puede ver la clase del cargador principal. Por ejemplo, la clase cargada por AppClassLoader puede ver la clase cargada por BootStrapClassLoader. La clase cargada por el cargador principal no puede ver la clase cargada por el cargador secundario. Por ejemplo, la clase cargada por BootStrapClassLoader no puede ver la clase cargada por AppClassLoader. Si no hay una relación padre-hijo directa o indirecta entre los dos cargadores, sus respectivas clases cargadas son invisibles entre sí. Por ejemplo, si personalizamos dos cargadores de clases, sus cargadores principales son ambos AppClassLoader, entonces las dos clases cargadas No son visibles entre sí.

La siguiente figura muestra la relación entre espacios de nombres:
Inserte la descripción de la imagen aquí
hay dos cargadores A y B que cargan la misma clase. Si no son una relación padre-hijo, A ha cargado la clase en la memoria y B también puede cargar la clase en la memoria. Pero si A y B tienen una relación padre-hijo, solo el que carga la clase primero puede cargar la clase en la memoria. Debido a que las condiciones de carga primero determinan si esta clase se ha cargado, si no se carga, se cargará, si se carga, no se volverá a cargar.

14 de septiembre de 2020

Supongo que te gusta

Origin blog.csdn.net/weixin_43907422/article/details/108550988
Recomendado
Clasificación