Herramientas para iniciar y desarrollar aplicaciones Java, compilación, ejecución en la JVM

No es ningún secreto que, por el momento, Java es uno de los lenguajes de programación más populares del mundo. La fecha oficial de lanzamiento de Java es el 23 de mayo de 1995.

Este artículo está dedicado a los conceptos básicos: describe las características básicas del lenguaje, lo que será útil para los "javists" principiantes, y los desarrolladores Java experimentados podrán actualizar sus conocimientos.

* El artículo fue preparado sobre la base de un informe de Eugene Freiman, desarrollador Java de IntexSoft.
El artículo contiene enlaces a materiales externos .





1. JDK, JRE, JVM


Java Development Kit es un kit de desarrollo de aplicaciones Java . Incluye las herramientas de desarrollo de Java y el Java Runtime Environment ( JRE ).

Las herramientas de desarrollo de Java incluyen alrededor de 40 herramientas diferentes: javac (compilador), java (lanzador de aplicaciones), javap (desensamblador de archivos de clase java), jdb (depurador java), etc.

El tiempo de ejecución de JRE es un paquete de todo lo necesario para ejecutar un programa Java compilado. Incluye la máquina virtual JVM y la Biblioteca de clases Java .

JVM es un programa diseñado para ejecutar bytecode. La primera ventaja de la JVM es el principio de "Escribir una vez, ejecutar en cualquier lugar" . Significa que una aplicación escrita en Java funcionará igual en todas las plataformas. Esta es una gran ventaja de JVM y Java.

Antes de Java, muchos programas de computadora se escribían para sistemas informáticos específicos, y se daba preferencia a la administración manual de memoria, como más eficiente y predecible. Desde la segunda mitad de la década de 1990, después del advenimiento de Java, la administración automática de memoria se ha convertido en una práctica común.

Hay muchas implementaciones de JVM, tanto comerciales como de código abierto. Uno de los objetivos de crear nuevas JVM es aumentar el rendimiento de una plataforma específica. Cada JVM se escribe por separado para la plataforma, mientras que es posible escribirlo para que funcione más rápido en una plataforma específica. La implementación de JVM más común es el punto de acceso JVM de OpenJDK . También hay implementaciones de IBM J9 , Excelsior JET .

2. Ejecución de código JVM


De acuerdo con la especificación Java SE , para que el código se ejecute en la JVM, debe completar 3 pasos:

  • Cargando bytecode e instanciando la clase Class
    En términos generales, para acceder a la JVM, la clase debe estar cargada. Hay clases de cargador separadas para esto, volveremos a ellas un poco más tarde.
  • Vinculación o vinculación
    Después de cargar la clase, comienza el proceso de vinculación, en el que se analiza y comprueba el código de bytes. El proceso de vinculación, a su vez, tiene lugar en 3 pasos:

    - verificación o verificación de bytecode: se verifica la exactitud de las instrucciones, la posibilidad de desbordamiento de pila en esta sección del código, la compatibilidad de los tipos de variables; la verificación ocurre una vez para cada clase;
    - preparación o preparación: en esta etapa, de acuerdo con la especificación, se asigna memoria para campos estáticos y se produce su inicialización;
    - resolución o resolución: resolución de enlaces simbólicos (cuando en bytecode abrimos archivos con la extensión .class, vemos valores numéricos en lugar de enlaces simbólicos).
  • Inicializando el objeto Class resultante
    En la última etapa, la clase que creamos se inicializa y la JVM puede comenzar a ejecutarla.

3. Cargadores de clases y su jerarquía.


De vuelta a los cargadores de clases, estas son clases especiales que forman parte de la JVM. Cargan clases en la memoria y las ponen a disposición para su ejecución. Los cargadores funcionan con todas las clases: tanto las nuestras como las que se necesitan directamente para Java.

Imagine la situación: escribimos nuestra solicitud y, además de las clases estándar, existen nuestras clases y hay muchas. ¿Cómo funcionará la JVM con esto? Java implementa la carga diferida de clases, en otras palabras, la carga diferida. Esto significa que la carga de las clases no se realizará hasta que en la aplicación no haya una llamada a la clase.

Jerarquía del cargador de clases





El cargador de primera clase es el cargador de clases Bootstrap . Está escrito en C ++. Este es el cargador base que carga todas las clases del sistema desde el archivo rt.jar . Al mismo tiempo, hay una ligera diferencia entre cargar clases desde rt.jar y nuestras clases: cuando la JVM carga clases desde rt.jar , no realiza todos los pasos de verificación que se realizan al cargar cualquier otro archivo de clase desde La JVM es consciente inicialmente de que todas estas clases ya están validadas. Por lo tanto, no debe incluir ninguno de sus archivos en este archivo.

El siguiente gestor de arranque es el cargador de clases de Extensión. Carga clases de extensión desde la carpeta jre / lib / ext . Suponga que desea que se cargue una clase cada vez que se inicia la máquina Java. Para hacer esto, puede copiar el archivo de clase de origen en esta carpeta y se cargará automáticamente.

Otro gestor de arranque es el cargador de clases del sistema . Carga las clases del classpath que especificamos cuando se inició la aplicación.

El proceso de carga de clases ocurre en una jerarquía:

  • En primer lugar, solicitamos una búsqueda en el caché del cargador de clases del sistema (el caché del cargador del sistema contiene clases que ya han sido cargadas por él);
  • Si la clase no se encuentra en el caché del cargador del sistema, miramos el cargador de clases de la Extensión de caché;
  • Si la clase no se encuentra en la memoria caché del cargador de extensiones, se solicita la clase del cargador Bootstrap.

Si la clase no se encuentra en el caché de Bootstrap, intenta cargar esta clase. Si Bootstrap no pudo cargar la clase, delega la carga de la clase al cargador de extensiones. Si en este punto se carga la clase, permanece en el caché del cargador de clases de Extensión y la carga de la clase se completa.

4. Estructura de archivos de clase y proceso de arranque


Procedemos directamente a la estructura de los archivos de clase.

Una clase escrita en Java se compila en un solo archivo con la extensión .class. Si hay varias clases en nuestro archivo Java, un archivo Java puede compilarse en varios archivos con la extensión .class - archivos de bytecode de estas clases.

Todos los números, cadenas, punteros a clases, campos y métodos se almacenan en el grupo Constante , el área de memoria del espacio Meta . La descripción de la clase se almacena en el mismo lugar y contiene el nombre, modificadores, superclase, superinterfaces, campos, métodos y atributos. Los atributos, a su vez, pueden contener cualquier información adicional.

Por lo tanto, al cargar clases:

  • lectura del archivo de clase, es decir, validación de formato
  • La representación de clase se crea en el grupo Constante (Meta espacio)
  • se cargan super clases y super interfaces; si no están cargados, entonces la clase en sí no se cargará

5. Ejecución de bytecode en la JVM


En primer lugar, para ejecutar bytecode, la JVM puede interpretarlo . La interpretación es un proceso bastante lento. En el proceso de interpretación, el intérprete "corre" línea por línea a través del archivo de clase y lo traduce en comandos que la JVM puede entender.

Además, la JVM puede transmitirlo , es decir compilar en código de máquina que se ejecutará directamente en la CPU.

Los comandos que se ejecutan con frecuencia no se interpretarán, pero se transmitirán de inmediato.

6. Compilación


Un compilador es un programa que convierte las partes fuente de los programas escritos en un lenguaje de programación de alto nivel en un programa de lenguaje de máquina que es "comprensible" para una computadora.

Los compiladores se dividen en:

  • No optimizando
  • Optimización simple (Hotspot Client): trabaje rápidamente, pero genere código no óptimo
  • Optimización compleja (Hotspot Server): realice transformaciones de optimización complejas antes de generar bytecode


Los compiladores también se pueden clasificar por tiempo de compilación:

  • Compiladores dinámicos
    Trabajan simultáneamente con el programa, lo que afecta el rendimiento. Es importante que estos compiladores se ejecuten en código que a menudo se ejecuta. Durante la ejecución del programa, la JVM sabe qué código se ejecuta con mayor frecuencia y, para no interpretarlo constantemente, la máquina virtual lo traduce inmediatamente en comandos que ya se ejecutarán directamente en el procesador.
  • Compiladores estáticos
    Compila más tiempo, pero genera el código óptimo para la ejecución. De los profesionales: no requieren recursos durante la ejecución del programa, cada método se compila utilizando optimizaciones.

7. Organización de la memoria en Java.


Una pila es un área de memoria en Java que funciona de acuerdo con el esquema LIFO: " Último en entrar - Fisrt fuera " o " Último en entrar , primero en salir ".



Es necesario para almacenar métodos. Las variables en la pila existen mientras se ejecute el método en el que fueron creadas.

Cuando se llama a cualquier método en Java, se crea un marco o área de memoria en la pila, y el método se coloca en la parte superior. Cuando un método completa la ejecución, se elimina de la memoria, liberando memoria para los siguientes métodos. Si la memoria de la pila está llena, Java lanzará una excepción java.lang.StackOverFlowError . Por ejemplo, esto puede suceder si tenemos una función recursiva que se llamará a sí misma y no habrá suficiente memoria en la pila.

Características clave de la pila:

  • La pila se llena y libera a medida que se llaman y completan nuevos métodos.
  • El acceso a esta área de memoria es más rápido que el montón.
  • El tamaño de la pila está determinado por el sistema operativo.
  • Es seguro para subprocesos, ya que cada pila tiene su propia pila separada.

Otra área de memoria en Java es Heap o heap . Se utiliza para almacenar objetos y clases. Siempre se crean nuevos objetos en el montón, y las referencias a ellos se almacenan en la pila. Todos los objetos en el montón tienen acceso global, es decir, se puede acceder a ellos desde cualquier lugar de la aplicación.

El montón se divide en varias partes más pequeñas llamadas generaciones:

  • Generación joven : el área donde se encuentran los objetos creados recientemente
  • Generación antigua (tenencia) : el área donde se almacenan los objetos "longevos"
  • Antes de Java 8, había otra área, la generación permanente , que contiene metainformación sobre clases, métodos y variables estáticas. Después del advenimiento de Java 8, se decidió almacenar esta información por separado, fuera del montón, es decir, en el espacio Meta




¿Por qué abandonó la generación permanente? En primer lugar, esto se debe a un error asociado con el desbordamiento del área: dado que Perm tenía un tamaño constante y no podía expandirse dinámicamente, tarde o temprano la memoria se agotó, se produjo un error y la aplicación se bloqueó.

El metaespacio tiene un tamaño dinámico, y en tiempo de ejecución puede expandirse a tamaños de memoria JVM.

Características clave del montón:

  • Cuando esta área de memoria está llena, Java arroja java.lang.OutOfMemoryError
  • El acceso al montón es más lento que el acceso a la pila
  • El recolector de basura trabaja para recolectar objetos no utilizados
  • Un montón, a diferencia de una pila, no es seguro para subprocesos, ya que cualquier subproceso puede acceder a él


Con base en la información anterior, considere cómo se realiza la administración de memoria utilizando un ejemplo simple:

public class App { public static void main(String[] args) { int id = 23; String pName = "Jon"; Person p = null; p = new Person(id, pName); } } class Person { int pid; String name; // constructors, getters/setters } 


Tenemos una clase de aplicación en la que el único método principal consiste en:

- variable de identificación primitiva de tipo int con valor 23
- pName variable de referencia de tipo String con valor Jon
- variable de referencia p de tipo persona



Como ya se mencionó, cuando se llama a un método, se crea un área de memoria en la parte superior de la pila en la que se almacenan los datos necesarios para almacenar este método.
En nuestro caso, esta es una referencia a la clase de persona : el objeto en sí se almacena en el montón y el enlace se almacena en la pila. También se inserta un enlace a la cadena en la pila, y la cadena en sí se almacena en el montón en el grupo de cadenas. La primitiva se almacena directamente en la pila.

Para llamar al constructor con los parámetros Person (String) del método main () en la pila, además de la llamada main () anterior, se crea un marco separado en la pila que almacena:

- esto - enlace al objeto actual
- valor de identificación primitivo
- la variable de referencia personName , que apunta a una cadena en el conjunto de cadenas.

Después de llamar al constructor, se llama a setPersonName () , después de lo cual se crea un nuevo marco en la pila nuevamente, donde se almacenan los mismos datos: referencia de objeto, referencia de línea, valor variable.

Por lo tanto, cuando se ejecuta el método de establecimiento, el marco desaparece, la pila se borra. A continuación, se ejecuta el constructor, se borra el marco que se creó para el constructor, después de lo cual el método main () finaliza su trabajo y también se elimina de la pila.

Si se llaman otros métodos, también se crearán nuevos marcos para ellos con el contexto de estos métodos específicos.

8. Recolector de basura


El recolector de basura está trabajando en el montón, un programa que se ejecuta en la máquina virtual Java que elimina los objetos a los que no se puede acceder.

Diferentes JVM pueden tener diferentes algoritmos de recolección de basura; también hay diferentes recolectores de basura.

Hablaremos sobre el colector GC más simple. Solicitamos la recolección de basura utilizando System.gc () .



Como se mencionó anteriormente, el montón se divide en 2 áreas: Nueva generación y Vieja generación.

La nueva generación (generación más joven) incluye 3 regiones: Eden , Survivor 0 y Survivor 1 .

La generación anterior incluye la región de tenencia .

¿Qué sucede cuando creamos un objeto en Java?

En primer lugar, el objeto cae en el Edén . Si ya hemos creado muchos objetos y no hay más espacio en el Edén , el recolector de basura dispara y libera memoria. Esta es la llamada recolección de basura pequeña : en la primera pasada, limpia el área del Edén y coloca los objetos "sobrevivientes" en la región Survivor 0 . Por lo tanto, la región del Edén está completamente liberada.

Si sucede que el área del Edén se ha vuelto a llenar, el recolector de basura comienza a trabajar con el área del Edén y el Superviviente 0 , que actualmente está ocupado. Después de la limpieza, los objetos sobrevivientes caerán en otra región: Survivor 1 , y los otros dos permanecerán limpios. Tras la posterior recolección de basura, Survivor 0 nuevamente se seleccionará como la región de destino. Por eso es importante que una de las regiones de Survivor esté siempre vacía.

La JVM supervisa los objetos que se copian constantemente y se mueven de una región a otra. Y para optimizar este mecanismo, después de un cierto umbral, el recolector de basura mueve dichos objetos a la región de tenencia .

Cuando no hay suficiente espacio para nuevos objetos en Tenured , hay una recolección de basura completa: Mark-Sweep-Compact .



Durante este mecanismo, se determina qué objetos ya no se usan, la región se borra de estos objetos y el área de memoria tenured se desfragmenta, es decir Secuencialmente lleno de los objetos necesarios.

Conclusión


En este artículo, examinamos las herramientas básicas del lenguaje Java: JVM, JRE, JDK, el principio y las etapas de la ejecución del código JVM, la compilación, la organización de la memoria, así como el principio del recolector de basura.

Source: https://habr.com/ru/post/471772/


All Articles