JavaRush /Blog Java /Random-ES /Cómo se cargan las clases en la JVM
Aleksandr Zimin
Nivel 1
Санкт-Петербург

Cómo se cargan las clases en la JVM

Publicado en el grupo Random-ES
Una vez completada la parte más difícil del trabajo del programador y escrita la aplicación "Hello World 2.0", solo queda montar la distribución y transferirla al cliente, o al menos al servicio de pruebas. En la distribución todo es como debe ser, y cuando lanzamos nuestro programa entra en escena la Máquina Virtual Java. No es ningún secreto que la máquina virtual lee los comandos presentados en archivos de clase en forma de código de bytes y los traduce como instrucciones para el procesador. Propongo comprender un poco sobre el esquema del código de bytes que ingresa a la máquina virtual.

Cargador de clases

Se utiliza para suministrar código de bytes compilado a la JVM, que generalmente se almacena en archivos con la extensión .class, pero también puede obtenerse de otras fuentes, por ejemplo, descargarse de la red o generarse por la propia aplicación. Cómo se cargan las clases en la JVM - 1De acuerdo con la especificación Java SE, para ejecutar el código en la JVM, debe completar tres pasos:
  • cargar código de bytes de recursos y crear una instancia de la claseClass

    Esto incluye buscar la clase solicitada entre las cargadas anteriormente, obtener el código de bytes para cargar y verificar su corrección, crear una instancia de la clase Class(para trabajar con ella en tiempo de ejecución) y cargar las clases principales. Si las clases e interfaces principales no se han cargado, entonces la clase en cuestión se considera no cargada.

  • vinculante (o vinculante)

    Según la especificación, esta etapa se divide en tres etapas más:

    • Verificación , se verifica la exactitud del código de bytes recibido.
    • Preparación , asignando RAM para campos estáticos e inicializándolos con valores predeterminados (en este caso, la inicialización explícita, si la hay, ya ocurre en la etapa de inicialización).
    • Resolución , resolución de enlaces simbólicos de tipos, campos y métodos.
  • inicializando el objeto recibido

    aquí, a diferencia de los párrafos anteriores, todo parece estar claro sobre lo que debe suceder. Por supuesto, sería interesante entender exactamente cómo sucede esto.

Todos estos pasos se realizan de forma secuencial con los siguientes requisitos:
  • La clase debe estar completamente cargada antes de vincularse.
  • Una clase debe probarse y prepararse completamente antes de inicializarse.
  • Los errores de resolución de enlaces ocurren durante la ejecución del programa, incluso si se detectaron en la etapa de enlace.
Como sabes, Java implementa la carga diferida (o diferida) de clases. Esto significa que la carga de clases de campos de referencia de la clase cargada no se realizará hasta que la aplicación encuentre una referencia explícita a ellos. En otras palabras, resolver enlaces simbólicos es opcional y no ocurre de forma predeterminada. Sin embargo, la implementación de JVM también puede utilizar carga de clases energética, es decir. todos los enlaces simbólicos deben tenerse en cuenta de inmediato. Es para este punto que se aplica el último requisito. También vale la pena señalar que la resolución de enlaces simbólicos no está ligada a ninguna etapa de carga de clases. En general, cada una de estas etapas constituye un buen estudio; intentemos descubrir la primera, es decir, cargar el código de bytes.

Tipos de cargadores de Java

Hay tres cargadores estándar en Java, cada uno de los cuales carga una clase desde una ubicación específica:
  1. Bootstrap es un cargador básico, también llamado Primordial ClassLoader.

    carga clases JDK estándar desde el archivo rt.jar

  2. Extension ClassLoader : cargador de extensiones.

    carga clases de extensión, que se encuentran en el directorio jre/lib/ext de forma predeterminada, pero se pueden configurar mediante la propiedad del sistema java.ext.dirs

  3. System ClassLoader : cargador del sistema.

    carga clases de aplicación definidas en la variable de entorno CLASSPATH

Java utiliza una jerarquía de cargadores de clases, donde la raíz es, por supuesto, la base. Luego viene el cargador de extensiones y luego el cargador del sistema. Naturalmente, cada cargador almacena un puntero al padre para poder delegarle la carga en caso de que él mismo no pueda hacerlo.

Clase abstracta ClassLoader

Cada cargador, a excepción del base, es descendiente de la clase abstracta java.lang.ClassLoader. Por ejemplo, la implementación del cargador de extensiones es la clase sun.misc.Launcher$ExtClassLoadery el cargador del sistema es sun.misc.Launcher$AppClassLoader. El cargador base es nativo y su implementación está incluida en la JVM. Cualquier clase que se extienda java.lang.ClassLoaderpuede proporcionar su propia forma de cargar clases con blackjack y estas mismas. Para ello es necesario redefinir los métodos correspondientes, que por el momento sólo puedo considerar superficialmente, porque No entendí este problema en detalle. Aquí están:

package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)Uno de los pocos métodos públicos, que es el punto de entrada para cargar clases. Su implementación se reduce a llamar a otro método protegido loadClass(String name, boolean resolve), que debe anularse. Si observa el Javadoc de este método protegido, podrá comprender algo como lo siguiente: se proporcionan dos parámetros como entrada. Uno es el nombre binario de la clase (o el nombre de clase completo) que debe cargarse. El nombre de la clase se especifica con una lista de todos los paquetes. El segundo parámetro es una bandera que determina si se requiere resolución de enlace simbólico. De forma predeterminada es false , lo que significa que se utiliza la carga diferida de clases. Además, según la documentación, en la implementación predeterminada del método, se realiza una llamada findLoadedClass(String name)que verifica si la clase ya se ha cargado previamente y, de ser así, devuelve una referencia a esta clase. De lo contrario, se llamará al método de carga de clases del cargador principal. Si ninguno de los cargadores pudo encontrar una clase cargada, cada uno de ellos, en orden inverso, intentará encontrar y cargar esa clase, anulando el archivo findClass(String name). Esto se discutirá con más detalle en el capítulo "Esquema de carga de clases". Y finalmente, por último pero no menos importante, una vez cargada la clase, dependiendo del indicador de resolución , se decidirá si se cargan las clases a través de enlaces simbólicos. Un claro ejemplo es que la etapa de Resolución se puede llamar durante la etapa de carga de clases. En consecuencia, al extender la clase ClassLoadery anular sus métodos, el cargador personalizado puede implementar su propia lógica para entregar código de bytes a la máquina virtual. Java también admite el concepto de cargador de clases "actual". El cargador actual es el que cargó la clase que se está ejecutando actualmente. Cada clase sabe con qué cargador se cargó y usted puede obtener esta información llamando a su archivo String.class.getClassLoader(). Para todas las clases de aplicaciones, el cargador "actual" suele ser el del sistema.

Tres principios de carga de clases

  • Delegación

    La solicitud para cargar la clase se pasa al cargador principal y se intenta cargar la clase en sí solo si el cargador principal no pudo encontrar ni cargar la clase. Este enfoque le permite cargar clases con el cargador que está lo más cerca posible del cargador base. Esto logra la máxima visibilidad de clase. Cada cargador mantiene un registro de las clases que cargó y las coloca en su caché. El conjunto de estas clases se llama alcance.

  • Visibilidad

    El cargador sólo ve "sus" clases y las clases "principales" y no tiene idea de las clases que fueron cargadas por su "secundario".

  • Unicidad

    Una clase sólo se puede cargar una vez. El mecanismo de delegación garantiza que el cargador que inicia la carga de clases no sobrecargue una clase que se cargó previamente en la JVM.

Por lo tanto, al escribir su gestor de arranque, un desarrollador debe guiarse por estos tres principios.

Esquema de carga de clases

Cuando se produce una llamada para cargar una clase, esta clase se busca en el caché de clases ya cargadas del cargador actual. Si la clase deseada no se ha cargado antes, el principio de delegación transfiere el control al cargador principal, que se encuentra en un nivel superior en la jerarquía. El cargador principal también intenta encontrar la clase deseada en su caché. Si la clase ya se ha cargado y el cargador conoce su ubicación, Classse devolverá un objeto de esa clase. De lo contrario, la búsqueda continuará hasta llegar al gestor de arranque base. Si el cargador base no tiene información sobre la clase requerida (es decir, aún no se ha cargado), se buscará el código de bytes de esta clase en la ubicación de las clases que el cargador dado conoce, y si la clase no puede se cargará, el control volverá al cargador secundario, que intentará cargar desde fuentes que conoce. Como se mencionó anteriormente, la ubicación de las clases para el cargador base es la biblioteca rt.jar, para el cargador de extensiones - el directorio con extensiones jre/lib/ext, para el sistema - CLASSPATH, para el usuario puede ser algo diferente . Por lo tanto, el progreso de la carga de clases va en la dirección opuesta: desde el cargador raíz hasta el actual. Cuando se encuentra el código de bytes de la clase, la clase se carga en la JVM y se obtiene una instancia del tipo Class. Como puede ver fácilmente, el esquema de carga descrito es similar a la implementación del método anterior loadClass(String name). A continuación puede ver este diagrama en el diagrama.
Cómo se cargan las clases en la JVM - 2

Como conclusión

En los primeros pasos para aprender un idioma, no es necesario comprender cómo se cargan las clases en Java, pero conocer estos principios básicos le ayudará a evitar la desesperación al encontrar errores como ClassNotFoundExceptiono NoClassDefFoundError. Bueno, o al menos entender aproximadamente cuál es la raíz del problema. Por lo tanto, se produce una excepción ClassNotFoundExceptioncuando una clase se carga dinámicamente durante la ejecución del programa, cuando los cargadores no pueden encontrar la clase requerida ni en el caché ni en la ruta de clase. Pero el error NoClassDefFoundErrores más crítico y ocurre cuando la clase requerida estaba disponible durante la compilación, pero no era visible durante la ejecución del programa. Esto puede suceder si el programa olvidó incluir la biblioteca que utiliza. Bueno, el hecho mismo de comprender los principios de la estructura de la herramienta que utiliza en su trabajo (no necesariamente una inmersión clara y detallada en sus profundidades) agrega cierta claridad a la comprensión de los procesos que ocurren dentro de este mecanismo, que, en a su vez, conduce a un uso seguro de esta herramienta.

Fuentes

Cómo funciona ClassLoader en Java En general, es una fuente muy útil con una presentación accesible de información. Cargando clases, ClassLoader Un artículo bastante extenso, pero con énfasis en cómo hacer tu propia implementación de cargador con estos mismos. ClassLoader: carga dinámica de clases Desafortunadamente, este recurso no está disponible ahora, pero allí encontré el diagrama más comprensible con un esquema de carga de clases, así que no puedo evitar agregarlo. Especificación de Java SE: Capítulo 5. Carga, vinculación e inicialización
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION