Outils de lancement et de développement d'applications Java, compilation, exécution sur la JVM

Ce n'est un secret pour personne que Java est actuellement l'un des langages de programmation les plus populaires au monde. La date de sortie officielle de Java est le 23 mai 1995.

Cet article est consacré aux principes de base: il décrit les fonctionnalités de base du langage, qui seront utiles pour les "javistes" débutants, et les développeurs Java expérimentés pourront rafraîchir leurs connaissances.

* L'article a été préparé sur la base d'un rapport d'Eugene Freiman - développeur Java d'IntexSoft.
L'article contient des liens vers des documents externes .





1. JDK, JRE, JVM


Java Development Kit est un kit de développement d'applications Java . Il comprend des outils de développement Java et Java Runtime Environment ( JRE ).

Les outils de développement Java comprennent environ 40 outils différents: javac (compilateur), java (lanceur d'applications), javap (désassembleur de fichiers de classe java), jdb (débogueur java), etc.

Le runtime JRE est un package de tout ce qui est nécessaire pour exécuter un programme Java compilé. Inclut la machine virtuelle JVM et la bibliothèque de classes Java .

JVM est un programme conçu pour exécuter le bytecode. Le premier avantage de la JVM est le principe de «Écrire une fois, exécuter n'importe où» . Cela signifie qu'une application écrite en Java fonctionnera de la même manière sur toutes les plateformes. C'est un gros avantage de la JVM et de Java lui-même.

Avant Java, de nombreux programmes informatiques étaient écrits pour des systèmes informatiques spécifiques, et la préférence était donnée à la gestion manuelle de la mémoire, comme plus efficace et prévisible. Depuis la seconde moitié des années 90, après l'avènement de Java, la gestion automatique de la mémoire est devenue une pratique courante.

Il existe de nombreuses implémentations JVM, à la fois commerciales et open source. L'un des objectifs de la création de nouvelles machines virtuelles Java est d'augmenter les performances d'une plate-forme spécifique. Chaque machine virtuelle Java est écrite séparément pour la plate-forme, tandis qu'il est possible de l'écrire afin qu'elle fonctionne plus rapidement sur une plate-forme spécifique. L'implémentation JVM la plus courante est le point d'accès JVM OpenJDK . Il existe également des implémentations d' IBM J9 , Excelsior JET .

2. Exécution de code JVM


Selon la spécification Java SE , pour exécuter le code dans la JVM, vous devez effectuer 3 étapes:

  • Chargement du bytecode et instanciation de la classe Class
    En gros, pour accéder à la JVM, la classe doit être chargée. Il existe des classes de chargeur distinctes pour cela, nous y reviendrons un peu plus tard.
  • Liaison ou liaison
    Après le chargement de la classe, le processus de liaison commence, sur lequel le bytecode est analysé et vérifié. Le processus de liaison, à son tour, se déroule en 3 étapes:

    - vérification ou vérification du bytecode: la justesse des instructions, la possibilité de débordement de pile sur cette section du code, la compatibilité des types de variables sont vérifiées; la vérification a lieu une fois pour chaque classe;
    - préparation ou préparation: à ce stade, conformément à la spécification, la mémoire est allouée aux champs statiques et leur initialisation a lieu;
    - résolution ou résolution: résolution des liens symboliques (lorsque dans le bytecode on ouvre des fichiers avec l'extension .class, on voit des valeurs numériques au lieu de liens symboliques).
  • Initialisation de l'objet Class résultant
    À la dernière étape, la classe que nous avons créée est initialisée et la JVM peut commencer à l'exécuter.

3. Chargeurs de classe et leur hiérarchie


Retour aux chargeurs de classe, ce sont des classes spéciales qui font partie de la JVM. Ils chargent les classes en mémoire et les rendent disponibles pour exécution. Les chargeurs fonctionnent avec toutes les classes: les nôtres et celles qui sont directement nécessaires pour Java.

Imaginez la situation: nous avons écrit notre application, et en plus des classes standard, il y a nos classes, et il y en a beaucoup. Comment la JVM fonctionnera-t-elle avec cela? Java implémente le chargement de classe différé, en d'autres termes le chargement paresseux. Cela signifie que le chargement des classes ne sera effectué que dans l'application, il n'y aura pas d'appel à la classe.

Hiérarchie du chargeur de classe





Le premier chargeur de classe est le chargeur de classe Bootstrap . Il est écrit en C ++. Il s'agit du chargeur de base qui charge toutes les classes système à partir de l'archive rt.jar . Dans le même temps, il existe une légère différence entre le chargement de classes à partir de rt.jar et nos classes: lorsque la JVM charge des classes à partir de rt.jar , elle n'effectue pas toutes les étapes de vérification qui sont effectuées lors du chargement d'un autre fichier de classe depuis La JVM est initialement consciente que toutes ces classes sont déjà validées. Par conséquent, vous ne devez inclure aucun de vos fichiers dans cette archive.

Le chargeur de démarrage suivant est le chargeur de classe Extension. Il charge les classes d'extension à partir du dossier jre / lib / ext . Supposons que vous souhaitiez qu'une classe se charge à chaque démarrage de la machine Java. Pour ce faire, vous pouvez copier le fichier de classe source dans ce dossier et il se chargera automatiquement.

Un autre chargeur de démarrage est le chargeur de classe système . Il charge les classes à partir du chemin de classe que nous avons spécifié au démarrage de l'application.

Le processus de chargement des classes se déroule dans une hiérarchie:

  • Tout d'abord, nous demandons une recherche dans le cache du chargeur de classe système (le cache du chargeur système contient des classes qui ont déjà été chargées par lui);
  • Si la classe n'a pas été trouvée dans le cache du chargeur système, nous regardons le chargeur de classe Extension cache;
  • Si la classe n'est pas trouvée dans le cache du chargeur d'extension, la classe est demandée au chargeur Bootstrap.

Si la classe n'est pas trouvée dans le cache Bootstrap, elle essaie de charger cette classe. Si Bootstrap n'a pas pu charger la classe, il délègue le chargement de la classe au chargeur d'extension. Si, à ce stade, la classe est chargée, elle reste dans le cache du chargeur de classe Extension et le chargement de la classe est terminé.

4. Structure des fichiers de classe et processus de démarrage


Nous passons directement à la structure des fichiers Class.

Une classe écrite en Java est compilée dans un seul fichier avec l'extension .class. S'il y a plusieurs classes dans notre fichier Java, un fichier Java peut être compilé en plusieurs fichiers avec l'extension .class - fichiers bytecode de ces classes.

Tous les nombres, chaînes, pointeurs vers les classes, champs et méthodes sont stockés dans le pool Constant - la zone de mémoire du méta-espace . La description de la classe est stockée au même endroit et contient le nom, les modificateurs, la super-classe, les super-interfaces, les champs, les méthodes et les attributs. Les attributs, à leur tour, peuvent contenir des informations supplémentaires.

Ainsi, lors du chargement des classes:

  • lecture du fichier de classe, c'est-à-dire validation du format
  • la représentation de classe est créée dans le pool constant (espace méta)
  • les super classes et les super interfaces sont chargées; s'ils ne sont pas chargés, la classe elle-même ne sera pas chargée

5. Exécution de bytecode sur la JVM


Tout d'abord, pour exécuter le bytecode, la JVM peut l' interpréter . L'interprétation est un processus assez lent. En cours d'interprétation, l'interpréteur «exécute» ligne par ligne le fichier de classe et le traduit en commandes compréhensibles par la JVM.

De plus, la JVM peut la diffuser , c'est-à-dire compiler en code machine qui sera exécuté directement sur le CPU.

Les commandes qui sont exécutées fréquemment ne seront pas interprétées, mais seront immédiatement diffusées.

6. Compilation


Un compilateur est un programme qui convertit les parties sources de programmes écrits dans un langage de programmation de haut niveau en un programme en langage machine «compréhensible» pour un ordinateur.

Les compilateurs sont divisés en:

  • Ne pas optimiser
  • Optimisation simple (Hotspot Client): travaillez rapidement, mais générez du code non optimal
  • Optimisation complexe (Hotspot Server): effectuez des transformations d'optimisation complexes avant de générer du bytecode


Les compilateurs peuvent également être classés par temps de compilation:

  • Compilateurs dynamiques
    Ils travaillent simultanément avec le programme, ce qui affecte les performances. Il est important que ces compilateurs fonctionnent sur du code qui est souvent exécuté. Lors de l'exécution du programme, la JVM sait quel code est le plus souvent exécuté, et afin de ne pas l'interpréter constamment, la machine virtuelle le traduit immédiatement en commandes qui seront déjà exécutées directement sur le processeur.
  • Compilateurs statiques
    Compilez plus longtemps, mais générez le code optimal pour l'exécution. Du côté des pros: ils ne nécessitent pas de ressources lors de l'exécution du programme, chaque méthode est compilée à l'aide d'optimisations.

7. Organisation de la mémoire en Java


Une pile est une région de mémoire en Java qui fonctionne selon le schéma LIFO - « Last in - Fisrt Out » ou « Last In, First Out ».



Il est nécessaire pour stocker des méthodes. Les variables sur la pile existent tant que la méthode dans laquelle elles ont été créées est exécutée.

Lorsqu'une méthode est appelée en Java, un cadre ou une zone de mémoire est créé sur la pile et la méthode est placée au sommet. Lorsqu'une méthode termine son exécution, elle est supprimée de la mémoire, libérant ainsi de la mémoire pour les méthodes suivantes. Si la mémoire de la pile est pleine, Java lèvera une exception java.lang.StackOverFlowError . Par exemple, cela peut se produire si nous avons une fonction récursive qui s'appellera et qu'il n'y aura pas assez de mémoire sur la pile.

Caractéristiques clés de la pile:

  • La pile est remplie et libérée à mesure que de nouvelles méthodes sont appelées et terminées.
  • L'accès à cette zone mémoire est plus rapide que le tas.
  • La taille de la pile est déterminée par le système d'exploitation.
  • Il est thread-safe, car chaque pile a sa propre pile distincte.

Un autre domaine de la mémoire en Java est le tas ou le tas . Il est utilisé pour stocker des objets et des classes. De nouveaux objets sont toujours créés sur le tas et leurs références sont stockées sur la pile. Tous les objets du tas ont un accès global, c'est-à-dire qu'ils sont accessibles depuis n'importe où dans l'application.

Le tas est divisé en plusieurs parties plus petites appelées générations:

  • Jeune génération - la zone où se trouvent les objets récemment créés
  • Ancienne génération (permanente) - la zone où les objets «à vie longue» sont stockés
  • Avant Java 8, il y avait un autre domaine - la génération permanente - qui contient des méta-informations sur les classes, les méthodes et les variables statiques. Après l'avènement de Java 8, il a été décidé de stocker ces informations séparément, en dehors du tas, notamment dans l'espace Meta




Pourquoi abandonner la génération permanente? Tout d'abord, cela est dû à une erreur qui était associée au débordement de la zone: puisque Perm avait une taille constante et ne pouvait pas se développer dynamiquement, tôt ou tard la mémoire était épuisée, une erreur a été levée et l'application s'est bloquée.

Le méta-espace a une taille dynamique et, à l'exécution, il peut s'étendre aux tailles de mémoire JVM.

Caractéristiques clés du tas:

  • Lorsque cette zone mémoire est pleine, Java lance java.lang.OutOfMemoryError
  • L'accès au tas est plus lent que l'accès à la pile
  • Le garbage collector fonctionne pour collecter les objets inutilisés
  • Un tas, contrairement à une pile, n'est pas sûr pour les threads, car n'importe quel thread peut y accéder


Sur la base des informations ci-dessus, considérez comment la gestion de la mémoire est effectuée à l'aide d'un exemple 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 } 


Nous avons une classe App dans laquelle la seule méthode principale consiste à:

- variable id primitive de type int avec la valeur 23
- Variable de référence pName de type String avec la valeur Jon
- variable de référence p de type personne



Comme déjà mentionné, lorsqu'une méthode est appelée, une zone mémoire est créée en haut de la pile dans laquelle sont stockées les données nécessaires au stockage de cette méthode.
Dans notre cas, il s'agit d'une référence à la classe person : l'objet lui-même est stocké sur le tas et le lien est stocké sur la pile. Un lien vers la chaîne est également placé sur la pile et la chaîne elle-même est stockée sur le tas dans le pool de chaînes. La primitive est stockée directement sur la pile.

Pour appeler le constructeur avec des paramètres Person (String) à partir de la méthode main () sur la pile, en plus de l'appel principal () précédent, une trame distincte est créée sur la pile qui stocke:

- ce - lien vers l'objet courant
- valeur id primitive
- la variable de référence personName , qui pointe vers une chaîne dans le String Pool.

Après avoir appelé le constructeur, setPersonName () est appelé, après quoi un nouveau cadre est à nouveau créé sur la pile, où les mêmes données sont stockées: référence d'objet, référence de ligne, valeur de variable.

Ainsi, lorsque la méthode de définition est exécutée, la trame disparaît, la pile est effacée. Ensuite, le constructeur est exécuté, le cadre qui a été créé pour le constructeur est effacé, après quoi la méthode main () termine son travail et est également supprimée de la pile.

Si d'autres méthodes sont appelées, de nouveaux cadres seront également créés pour eux avec le contexte de ces méthodes spécifiques.

8. Collecteur d'ordures


Le garbage collector travaille sur le tas - un programme exécuté sur la machine virtuelle Java qui se débarrasse des objets inaccessibles.

Différentes JVM peuvent avoir différents algorithmes de récupération de place; il existe également différents récupérateurs de place.

Nous parlerons du collecteur Serial GC le plus simple. Nous demandons la récupération de place à l'aide de System.gc () .



Comme mentionné ci-dessus, le tas est divisé en 2 zones: nouvelle génération et ancienne génération.

La nouvelle génération (jeune génération) comprend 3 régions: Eden , Survivor 0 et Survivor 1 .

L'ancienne génération comprend la région de tenure .

Que se passe-t-il lorsque nous créons un objet en Java?

Tout d'abord, l'objet tombe en Eden . Si nous avons déjà créé de nombreux objets et qu'il n'y a plus d'espace dans Eden , le garbage collector se déclenche et libère de la mémoire. Il s'agit de la soi-disant petite collecte des ordures - lors du premier passage, elle nettoie la zone d' Eden et place les objets «survivants» dans la région Survivor 0 . Ainsi, la région d' Eden est complètement libérée.

S'il arrive que la zone Eden soit à nouveau pleine, le ramasse-miettes commence à travailler avec la zone Eden et Survivor 0 , qui est actuellement occupé. Après le nettoyage, les objets survivants tomberont dans une autre région - Survivor 1 , et les deux autres resteront propres. Lors de la prochaine collecte des ordures, Survivor 0 sera à nouveau sélectionné comme région de destination. C'est pourquoi il est important qu'une des régions Survivor soit toujours vide.

La JVM surveille les objets qui sont constamment copiés et déplacés d'une région à l'autre. Et afin d'optimiser ce mécanisme, après un certain seuil, le garbage collector déplace ces objets vers la région Tenured .

Lorsqu'il n'y a pas assez d'espace pour de nouveaux objets dans Tenured , il y a une collecte complète des déchets - Mark-Sweep-Compact .



Au cours de ce mécanisme, il est déterminé quels objets ne sont plus utilisés, la région est débarrassée de ces objets et la zone de mémoire enregistrée est défragmentée, c'est-à-dire successivement remplis des objets nécessaires.

Conclusion


Dans cet article, nous avons examiné les outils de base du langage Java: JVM, JRE, JDK, le principe et les étapes de l'exécution du code JVM, la compilation, l'organisation de la mémoire, ainsi que le principe du garbage collector.

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


All Articles