- La clase Java y el código binario
Ya sabemos que cada clase Java que viene compilado, se
transforma en exactamente 1 file .class y nuestro código Java se transforma en el código
intermedio (bytecode Java) de bajo nivel a traverso del compilador javac. El file class consiste de :
- La tabla de los constantes (son los valores constantes que aparecen en la clase y los punteros que se refieren a otros clases, métodos y campos).
- La descripción de la clase (nombres, modificadores, super clase/interface, campos, métodos y atributos)
El código binario (bytecode) esta considerado como un atributo de un método.
JVM funciona a traverso de las pilas:
- La pila de las instrucciones
- La pila de los operandos
- La pila de los variables locales
Lo que estamos haciendo aquí es:
- Cargamos un integer desde el registro numero 3
- Metemos una constante en el registro 5
- Sumamos
- Guardamos el resultado en el registro 4
La diferencia principal entre un código Java normal y el código intermedio es la tipificación estricta. P.e. el prefijo "i"- significa Integer y es la llave para ser compatible con varias plataformas.
Java Runtime
Sabemos que cada programa ejecutada por la JVM necesita 3 cosas:- Método "main()".
- Classpath
- En el mundo de las aplicaciones Web, será el servidor Web (p.e. Tomcat) que actúa como el programa para la JVM y este servidor tiene su método "main()", su classpath, etc.
- JVM
- La biblioteca Java estándar (j.l.Object, j.l.String, IO, NET, NIO, Awt/Swing, etc)
- Métodos nativos (relacionados a la OS específica)
- Clases suplementarios (TimeZone, Media, etc)
Classloading Engine
De donde carga este motor las clases para la ejecución?
- desde la Java Runtime (biblioteca estándar)
- Classpath de la aplicación
- Auto-generados ("on the go") - Proxy, Reflection, invoke dynamic
- Clases proporcionadas por la aplicación
Cada clase viene cargada con el ayuda de un cargador de las clases.
- La Biblioteca estándar viene cargada con el cargador bootstrap
- Las bibliotecas de la classpath - con AppClassLoader
- Las clases de la aplicación pueden crear sus proprios cargadores que pueden cargar las clases
Que es lo que pasa se lanza la JVM:
- El cargador del sistema carga la clase principal .class que contiene el método main(). Al mismo tiempo se carga la biblioteca estándar y las bibliotecas de la classpath.
- Se verifica el código binario
- Si el formato esta ok, se crea una representación Runtime de esta clase en los registros de memoria Meta Space
- Se cargan las superclases o superinterfaces
- El método main() viene ejecutado
Verification del código binario:
La verificación del formato del código binario (bytecode) es muy importante. Todos los enlaces en bytecode de la file .class que se refieren a los campos o métodos de la clase, son simbólicos, para ejecutarlo, deben ser resueltos. Se estos enlaces no pueden ser resueltos - una excepción sera producida durante la ejecución de la JVM.
Ahora una pregunta: que puede pasar, si intentamos ejecutar el código siguiente, donde intentamos
meter en el mismo registro dos números, los sumar y redirigir el programa al inicio?
Respuesta 1: entraremos en un bucle infinito.
Respuesta 2: StackOverflowError
Respuesta 3: VerifyError
Respuesta 4: el codigo no sera ejecutado
La respuesta correcta es 4. La verificación del código se hace antes de cualquiera ejecución del código. Se hace solo 1 vez y, si en este momento, cualquier fragmento del codigo no esta bien - todo la clase sera marcado "no verificado" y ningun otro metodo de la clase viene ejecutado.
Pero claro, también podemos lanzar nuestro codigo con "-Xverify:none":
En este caso obtenemos el error StackOverflow y la JVM produce un error interno. Es un gran ventaja que la JVM verifica el código antes de ejecutarlo, lo que hace a la JVM muy eficiente (mas eficiente que un programa en p.e. C++ donde hay mucha potencial para errores, pq el código no viene verificado).
Execution Engine
Sera la próxima etapa después de la verificación.
- Un inicializador estático de la clase viene llamado, cuando se llama o el método new(), o el método estático.
- Se inicializa la superclase o superinterface con los métodos predeterminados (default).
La ejecución de byte code en la JVM pasa en 2 maneras:
- Con un interpretador: "paso-por-paso"
Desde la 1ra instrucción (opcode) en el bytecode, toma la instrucción, mira el numero de los argumentos que espera una instrucción (p.e. 2 números para sumar) , ejecuta la instrucción (por ejemplo, suma 2 constantes, mete el resultado en un registro ) y sigue así por la instrucción siguiente. Pero esa manera para ejecutar el bytecode es muy lenta.
- Con un compilador JIT/AOT
En este caso, en vez de ir "paso-por-paso", todo el código binario del programa viene compilado a codigo maquina a la manera dinámica o estática que permite mejorar el rendimiento de sistemas de programación. Se utiliza para optimizar varias partes "pesadas" del código binario para un procesador o un sistema concreto (p.e. HotSpot). Varios compiladores pueden trabajar en la manera dinámica (JIT, just-in-time), en tiempo de ejecución o anticipada (AOT, ahead-of-time). La manera dinámica (JIT) puede ser aplicada p.e. para optimizar la ejecución de las expresiones que aparecen regularmente en el código (hot code), pero ralentiza la ejecución del programa, porque compila en tiempo de ejecución. Los compiladores AOT compilan antes de la ejecución y no tienen limites en tiempo o recursos para optimizar el programa. En este caso el programa esta mejor optimizada y al momento de la ejecución el codigo ya esta listo y compilado -> no se pierden los recursos.
En este sentido el código binario (bytecode) puede ser considerado como intermedio entre el código Java y el código maquina.
código Java -> javac -> byte code -> JIT (AOT) -> codigo maquina.
Meta Information
El concepto de "java reflection" permite a la JVM de acceder a los métodos del código Java con su nombre. Para realizar eso, la JVM utiliza "meta space". Se utiliza mucho varios framework y varios lenguajes que utilizan la JVM (Ruby, Groovy). Para invocar los métodos nativos relacionados al sistema operativo concreto se utiliza JNI (Java Native Interface). Este framework nos permite de ejecutar los métodos del código C o C++ para acceder a los recursos dependientes de la plataforma, p.e. Java IO, Java .NET o AWT/Swing.
Thread Synchronisation
Para ejecutar el código java a la manera mas eficaz se utilizan los hilos de ejecución (threads). Cada hilo Java corresponde a 1 hilo del sistema 1:1. La memoria que se utiliza para los variables locales y los argumentos de los métodos (method frames) se llama "pila" o "stack". Cada hilo de ejecución en una aplicación Java tiene su propia pila y el tamaño de esta pila esta definido por el parámetro siguiente:
-Xss.
Todos los métodos llamados por la JVM vienen trazados en la "stack trace". Stack trace funciona a la misma manera como funciona en el código nativo como C. JVM busca un handler cuando una excepción viene producida por el código y, si lo encuentra le pasa la gestión.
Ahora miramos que va pasar cuando tenemos 2 hilos que interactúan entre ellos:
Aquí tenemos 2 hilos: uno carga los datos, el otro espera estos datos. A traverso de un argumento booleano "Shared.ready" el 2ndo hilo va entender cuando el primero acaba de cargar los datos. El problema es que la optimización puede cambiar el orden de la ejecución de las instrucciones y el hilo #2 (Thread 2) puede recibir Shared.ready=true desde el hilo #1 antes que shared.data viene cargada. Para evitar eso hay un mecanismo en el modelo de memoria Java (Java Memory Model).
Podemos declarar "Shared.ready" como volatile - y eso sera un señal para la JVM que el orden de la ejecución no se puede cambiar. Así garantizamos que los datos vienen cargados antes que shared.ready será igual a "true". También un método puede ser monitoreado por un thread, si lo declaramos "synchronized".
Recolector de basura - gestión de memoria (Garbage Collection).
Alocación de los objetos de la memoria:
Cuando un nuevo objeto viene creado con el operador "new", la JVM inicializa este objeto y asigna la memoria para todas las instancias de este objeto, esta memoria se llama "java heap". La JVM pregunta la memoria al sistemo operativo, si hay espacio libre. Si no hay espacio libre, la JVM inicia el proceso de la "recolección de basura". También podemos forzar al programa de iniciar la recolección manualmente.
Thread-safe (seguridad de los hilos)
Para que un hilo de ejecución no "come" la memoria al otro hilo de ejecución, existe "thread local heap" - cada hilo "come" solo su parte de la memoria alocada.
Basura.
Que es lo que basura en sentido Java? Los objetos sin enlaces son basura. Pero que pasa si objeto son relacionados entre ellos? En este sentido es mejor definir lo que no es basura (GC Roots):
- Todos los objetos en los campos estáticos de las clases (variables estáticos)
- Todos los objetos accesibles desde los hilos de ejecución (Threads)
- Todos los objetos de los enlaces JNI en los métodos nativos
- Todos los objetos que referencian los primeros 3
Como ejemplo, los GC Roots de una aplicación simple serán:
- Variables locales del método main()
- El hilo principal de ejecución (main thread)
- Variables estáticos de la clase principal
Todo el resto es basura.
Para determinar qué objetos ya no están en uso, la JVM funciona a traverso de un algoritmo que se llama "marcar y barrer" (mark-and-sweep). Como se puede intuir, tiene 2 pasos:
1) Marcar: el algoritmo recorre todas las referencias a objetos, a partir de las raíces de GC, y marca cada objeto encontrado como vivo.
2) Barrer: toda la memoria heap que no está ocupada por los objetos marcados se recupera. Simplemente se marca como libre, esencialmente libre de objetos no utilizados.
El efecto de "marcar y barrer" son pequeñas micro-interrupciones (que gracias a los sistemas nuevas con mucho espacio y mucha optimización se han hecho casi invisibles) que ocurren cuando el recolector de basura hace un trabajo. Estas interrupciones eran responsables por los "lags" en los primeros aparatos Android. Ahora los aparatos Android se han hecho mucho mas potentes y los "lags" no son tan obvios y son casi innotables. También fueran desarrolladas varias estrategias para disminuir estas interrupciones:
- Estrategia incremental: recoger solo una parte de la basura durante la interrupción
- Estrategia parallel: recoger la basura en varios threads a la vez
- Estrategia "concurenta ": recoger la basura durante el funcionamiento del programa
En el pasado había otra estrategia para recoger la basura, se llamaba "stop-and-copy". Con esta estrategia todos los objetos "vivos" eran copiados en otro espacio heap vacío, con lo cual en el primer espacio solo quedaban objetos "muertos" que estaban para ser recogidos. Pero esta estrategia era menos eficaz y ralentizaba mucho la recolección de basura en la aplicación.
Monitoring
Visto que la JVM sabe todo sobre nuestra aplicación, tiene varios almacenes para monitorear le ejecución de esta aplicación:
- JVM Tool Interface - es una interfaz que viene utilizada, p.e. por el debugger, cuando intentamos a depurar nuestro programa.
- Java Management Beans - una colección de la información sobre la cantidad de nuestros objetos, cuando fueron creados, cuanta memoria fue alocada para ellos, etc. Herramientos como JConsole o Visual VM toman la información desde Java Management Beans.
Resumen
Tenemos bytecode o los métodos nativos que pueden o entrar en la JVM directamente o primero ser compilado en el código maquina y después ser ejecutado directement en el procesador. En el tiempo de la ejecución se organiza la JVM, la memoria viene alocada según las necesidades del programa (en verdad un pelin mas), A traverso de la meta informacion tenemos acceso a todos los elementos de nuestro programa durante la ejecución.
- JVM es una cosa muy complicada, pero bastante interesante
- Gracias a la JVM, Java es bastante flexible, pero eficaz
- Las tecnologías JVM se están mejorando de un dia a otro.
Realizaciones de JVM.
- Oracle HotSpot (mas común)
- IBM J9 (utilizada en Websphere)
- Excelsior JET (tiene AOT)
- Azul (basada en HotSpot, han cambiado el GC)
- SAP, Redhat (adaptados a sus sistemas).
No hay comentarios:
Publicar un comentario