Maison  >  Article  >  Java  >  L'histoire de la vie des programmes Java

L'histoire de la vie des programmes Java

怪我咯
怪我咯original
2017-04-05 16:45:031276parcourir

En tant que programmeurs, nous écrivons du Code tous les jours, mais comprenez-vous vraiment son cycle de vie ? Aujourd'hui, parlons brièvement de son histoire de vie. En parlant d'un morceau de code Java, de sa naissance à la fin du jeu, il peut être grossièrement divisé en les étapes suivantes : compilation, chargement de classe, exécution et GC.

Compilation

La période de compilation du langage Java est en fait un processus « incertain », car il peut s'agir d'un front-end compilateur Le processus de conversion des fichiers .java en fichiers .class ; il peut également faire référence au processus du compilateur d'exécution back-end de la JVM (compilateur JIT) convertissant le bytecode en code machine, il peut également faire référence à l'utilisation de statique ; advance Le compilateur (compilateur AOT) compile directement le fichier .java en code machine local. Mais nous parlons ici de la première catégorie. Cela correspond également à notre compréhension publique de la compilation. Par quels processus la compilation a-t-elle suivi pendant cette période ?

Analyse lexicale et syntaxique

L'analyse lexicale consiste à convertir le flux de caractères du code source en un ensemble de jetons, tandis que l'analyse syntaxique est le processus de construction abstraite d'un arbre syntaxique (ATS) basé sur la séquence de jetons. ATS est une représentation arborescente utilisée pour décrire la structure syntaxique du code du programme. Chaque nœud de l'arbre syntaxique représente une structure syntaxique dans le code du programme, telle que les packages, les types, les modificateurs, les opérateurs, Interface, valeur de retour et même codeCommentaires peuvent être une structure syntaxique.

Remplissage de la table des symboles

Après avoir terminé l'analyse syntaxique et lexicale, l'étape suivante est le processus de remplissage de la table des symboles. Les informations enregistrées dans la table des symboles seront utilisées à différentes étapes de. compilation. Étendons ici le concept de table de symboles. Qu'est-ce qu'une table de symboles ? Il s'agit d'une table composée d'un ensemble d'adresses de symboles et d'informations sur les symboles. La plus simple peut être comprise comme la forme de paires de valeurs K-V d'une table de hachage. Pourquoi les tables de symboles sont-elles utilisées ? L'une des premières applications des tables de symboles consistait à organiser les informations sur le code du programme. Au départ, les programmes informatiques n'étaient que de simples chaînes de chiffres, mais les programmeurs ont vite découvert qu'il était beaucoup plus pratique d'utiliser des symboles pour représenter les opérations et les adresses mémoire (noms de variables). L'association de noms et de numéros nécessite une table de symboles. À mesure que les programmes se développent, les performances des opérations sur les tables de symboles deviennent progressivement un goulot d'étranglement pour l'efficacité du développement des programmes. Pour cette raison, de nombreuses structures de données et algorithmes sont nés pour améliorer l'efficacité des tables de numéros de séquence. Quant aux soi-disant structures de données et algorithmes, que sont-ils ? De manière générale : recherche séquentielle dans des listes chaînées non ordonnées, recherche binaire dans des tableaux ordonnés, arbres de recherche binaires, arbres de recherche équilibrés (on entre ici principalement en contact avec des arbres rouge-noir), tables de hachage (hachage basé sur la méthode zipper), listes, tables de hachage basé sur un sondage linéaire). Comme java.util.TreeMap et java.util.HashMap en Java, ils sont implémentés respectivement sur la base des tables de symboles des arbres rouge-noir et des tables de hachage à glissière. Le concept de table de symboles évoqué ici ne sera pas expliqué en détail. Ceux qui sont intéressés peuvent y trouver des informations pertinentes.

Analyse sémantique

Après les deux étapes précédentes, nous avons obtenu la représentation de l'arbre syntaxique abstrait du code du programme. L'arbre syntaxique peut représenter une abstraction correcte du code source, mais il n'y a aucune garantie que le code source soit abstrait. Le programme source est logique. Oui, c'est à ce moment-là que l'analyse sémantique entre en scène. Sa tâche principale est d'examiner la nature contextuelle du programme source structurellement correct. La vérification des annotations, l'analyse des flux de données et de contrôle et le décodage du sucre syntaxique sont plusieurs étapes de l'étape d'analyse sémantique. Nous discuterons ici en détail du concept de sucre syntaxique. Le sucre syntaxique fait référence à une certaine syntaxe ajoutée à un langage informatique. Cette syntaxe n'a aucun impact sur la fonctionnalité du langage, mais est plus pratique à utiliser pour les programmeurs. Les sucres syntaxiques les plus couramment utilisés en Java sont les génériques, les paramètres de longueur variable, l'auto-boxing/unboxing et la traverséeloop. La JVM ne prend pas en charge ces syntaxes au moment de l'exécution et elles reviennent à des bases simples lors de la compilation. Phase de structure grammaticale, ce processus consiste à résoudre le sucre syntaxique. Pour donner un exemple d'effacement générique, List et List seront effacés de manière générique après la compilation et deviendront le même type natif List.

 Génération de bytecode

La génération de bytecode est la dernière étape du processus de compilation Javac. À ce stade, les informations générées lors des étapes précédentes seront converties en bytecode et écrites sur le disque. une petite quantité de travail d’ajout de code et de conversion a été effectuée. Méthode constructeur d'instance () et méthode constructeur de classe () (le constructeur d'instance ici ne fait pas référence au constructeur par défaut , si le code utilisateur ne fournit aucun constructeur, alors le Le compilateur ajoutera un constructeur par défaut sans paramètres et avec la même accessibilité que la classe actuelle. Ce travail a été terminé lors de l'étape de remplissage de la table des symboles, et la méthode du constructeur de classe () fait référence au compilateur collectant automatiquement la classe All. Les actions d'affectation de variables de classe et les instructions dans les blocs d'instructions statiques sont fusionnées dans l'arborescence syntaxique à ce stade. À ce stade, tout le processus de compilation se termine.

Chargement des classes

Compilation Après avoir compilé le programme en bytecode, l'étape suivante est le processus de chargement des classes en mémoire.

Le processus de chargement de classe est effectué dans la zone méthode de la mémoire de la machine virtuelle, qui implique la mémoire de la machine virtuelle, nous introduisons donc ici d'abord brièvement le concept de distribution de programme dans la zone mémoire. La zone mémoire de la machine virtuelle est divisée en : compteur de programme, pile, pile de méthodes locales, tas, zone de méthodes (certaines zones sont des pools de constantes d'exécution) et mémoire directe.

Compteur de programme

Le compteur de programme est un petit espace mémoire Il peut être considéré comme un indicateur de numéro de ligne du bytecode exécuté par le thread actuel. Dans le concept JVM modèle , l'interpréteur de bytecode fonctionne en modifiant la valeur de ce compteur pour sélectionner la prochaine instruction de bytecode qui doit être exécutée.

Pile

La pile est utilisée pour stocker des tables de variables locales, des piles d'opérandes, des liens dynamiques, des sorties de méthode et d'autres informations. La table de variables locales stocke divers types de données et objetsréférences de base qui sont restreints lors de la compilation. Comme le compteur de programme, il est privé du thread.

Pile de méthodes locales

La pile de méthodes locales est similaire à la pile de machines virtuelles présentée ci-dessus. Leur différence est que la pile de machines virtuelles sert à la machine virtuelle pour exécuter des méthodes Java (bytecode) et. La pile de méthodes locales sert les méthodes natives utilisées par la machine virtuelle, et certaines machines virtuelles combinent même les deux en une seule.

Heap

Le tas est le plus grand morceau de mémoire géré par la JVM. Il s'agit d'une zone partagée par tous les threads. Son seul objectif est de stocker les instances d'objet. Presque toutes les instances d'objet allouent de la mémoire ici (comme les objets de classe spéciale, la mémoire est allouée dans la zone de méthode). Cet endroit est également le principal domaine de gestion du garbage collection. Du point de vue du recyclage de la mémoire, les garbage collector utilisent désormais des algorithmes de collecte générationnelle (seront présentés en détail plus tard), de sorte que le tas Java peut être subdivisé davantage : la nouvelle génération et l'ancienne. génération et la nouvelle génération La génération est subdivisée en : espace Eden, espace Du Survivant et Espace Vers le Survivant. Pour des raisons d'efficacité, le tas peut également être divisé en plusieurs tampons d'allocation privés de thread (TLAB). Quelle que soit la façon dont elles sont divisées, cela n'a rien à voir avec le contenu du stockage. Quelle que soit la zone, les instances d'objets sont toujours stockées. Le but de leur existence est uniquement de mieux recycler et allouer la mémoire.

Zone de méthode

La zone de méthode, comme le tas, est une zone de mémoire partagée par les threads. Elle est utilisée pour stocker les informations de classe, les constantes, les variables statiques et le compilateur juste à temps. compilation qui ont été chargées par le code de la machine virtuelle et d’autres données. Le pool de constantes d'exécution fait partie de la zone des méthodes. Il est principalement utilisé pour stocker divers littéraux et références de symboles déclarés au moment de la compilation.

Mémoire directe

La mémoire directe ne fait pas partie de la zone de données d'exécution de la machine virtuelle. C'est également une zone de mémoire non définie dans la spécification Java. Vous pouvez simplement la comprendre comme de la mémoire hors tas. L'allocation de mémoire n'est pas affectée par la taille du tas Java, mais est limitée par la taille totale de la mémoire.

Après avoir parlé du concept de zone mémoire de machine virtuelle, revenons au sujet, quel est le processus de chargement des classes ? Cinq étapes : chargement, vérification, préparation, analyse et initialisation. Le chargement, la vérification, la préparation et l'initialisation sont exécutés séquentiellement, mais l'analyse n'est pas nécessairement le cas. Elle peut être exécutée après l'initialisation.

Chargement

Pendant la phase de chargement, la JVM doit effectuer trois étapes : d'abord, obtenir le flux d'octets binaires qui définit cette classe via le nom complet de la classe, puis convertir le flux d'octets représenté par ceci La structure de stockage statique est convertie en structure de données d'exécution de la zone de méthode, et enfin un objet java.lang.Class représentant cette classe est généré dans la mémoire, qui sert de diverses entrées de données pour cette classe dans le zone de méthode. Dans la première étape d'obtention du flux d'octets binaires, il n'est pas clairement indiqué de l'obtenir à partir d'un fichier *.class. La flexibilité de la réglementation nous permet de l'obtenir à partir du ZIP (qui sert de base au format JAR, EAR/WAR). ) et l'obtenir depuis le réseau. (Applet), calculé et généré au moment de l'exécution (proxy dynamique), autres fichiers générés (classe Class générée par le fichier JSP), obtenus à partir de la base de données.

Vérification

La vérification, comme son nom l'indique, consiste en fait à garantir que les informations contenues dans le flux d'octets du fichier Class répondent aux exigences de la JVM, car la source du fichier Class n'est pas nécessairement générée à partir de le compilateur, et peut également être généré à l'aide de l'éditeur hexadécimal qui écrit directement les fichiers de classe. Le processus de vérification comprend la vérification du format de fichier, la vérification des métadonnées et la vérification du bytecode. Les méthodes de vérification de sécurité spécifiques ne seront pas détaillées ici.

Préparation

L'étape de préparation est l'étape où la mémoire est formellement allouée aux variables de classe et où les valeurs initiales sont définies. La mémoire utilisée par ces variables est allouée dans la zone de méthode.

Analyse

La phase d'analyse est le processus dans lequel la JVM remplace la référence de symbole dans le pool de constantes par une référence directe (un pointeur vers la cible, un décalage relatif ou un handle). Le remplissage de compilation dont nous avons parlé plus tôt La valeur de la table des symboles est reflétée ici. Le processus d'analyse n'est rien d'autre que l'analyse de classes ou d'interfaces, de champs et de méthodes d'interface.

Initialisation

La phase d'initialisation de la classe est la dernière étape du processus de chargement de la classe. Dans la phase de préparation, les variables se sont vu attribuer une valeur initiale, et dans cette étape, elle sera effectuée. selon les exigences personnalisées par le programmeur. Initialisez les variables de classe et d'autres ressources. À ce stade, il s'agit du processus d'exécution de la méthode () mentionnée dans le processus de génération de bytecode compilé précédent. La machine virtuelle garantit également que lorsque cette méthode est appelée simultanément dans un environnement multithread, elle est correctement verrouillée et synchronisée, garantissant qu'un seul thread exécute cette méthode pendant que les autres threads bloquent et attendent. Exemple Java simple dans Singleton En parlant de concurrence, la méthode d'écriture thread-safe de singleton basée sur l'initialisation de classe est liée à cela. Si vous êtes intéressé, vous pouvez la combiner et y jeter un œil. Cet endroit implique également un autre point de connaissance qui nous préoccupe davantage. Quand Java déclenche-t-il l'opération d'initialisation de la classe ?

  • Lorsque vous rencontrez les quatre instructions de bytecode new, getstatic, putstatic ou Ensurestatic, si la classe n'a pas été initialisée, son initialisation doit être déclenchée. Quelles sont les différentes instructions fork devant. une compréhension simple est lors de la création d'un objet, lors de la lecture ou de la définition du champ statique d'une classe, lors de l'appel de la méthode statique d'une classe.

  • Lors de l'utilisation de la méthode du package java.lang.reflect pour effectuer un appel réflexif à une classe, si la classe n'est pas initialisée, son initialisation doit être déclenchée.
    Lors de l'initialisation d'une classe et de la constatation que sa classe parent n'a pas encore été initialisée, l'opération d'initialisation de sa classe parent sera déclenchée en premier.

  • Lorsque la machine virtuelle démarre, l'utilisateur doit spécifier une classe principale à exécuter (la classe où se trouve la méthode principale), et la machine virtuelle initialisera d'abord cette classe principale .

  • Lors de l'utilisation de la prise en charge du langage dynamique au-dessus de JDK1.7, si le résultat final de l'analyse d'une instance java.lang.invoke.MethodHandle est le handle de méthode de REF_getStatic, REF_putStatic, REF_invokeStatic, et ceci Si la classe correspondant au handle de méthode n’a pas été initialisée, l’opération d’initialisation sera déclenchée.

Exécuter

Après les deux étapes ci-dessus, le programme commence à s'exécuter normalement. Nous savons tous que le processus d'exécution du programme implique les opérations de calcul de diverses instructions. le programme ? Qu'en est-il de l'exécution ? C'est ici que seront utilisés le compilateur back-end (compilateur JIT juste-à-temps) + interpréteur évoqué en début d'article (la machine virtuelle HotSpot utilise par défaut un interpréteur et un compilateur), et l'exécution du bytecode. responsable des tâches de diverses opérations de calcul du programme. Lors de l'exécution du code Java, il peut avoir deux options : l'exécution interprétée (exécutée via un interpréteur) et l'exécution compilée (code local généré via un compilateur juste à temps) ou peut-être les deux. Le cadre de pile est une structure de données utilisée pour prendre en charge l'appel de méthode et l'exécution de machines virtuelles. Les idées de calcul spécifiques de diverses instructions pour pousser et faire éclater la pile impliquent un algorithme classique-Dijkstra. Quant à la façon de l'exécuter, si vous êtes intéressé, vérifiez les informations vous-même. Cet endroit ne va pas trop loin. Les problèmes d'optimisation du temps d'exécution sont tout aussi importants à ce stade, et l'équipe de conception JVM a concentré l'optimisation des performances à ce stade, afin que les fichiers de classe non générés par Javac puissent également bénéficier des avantages de l'optimisation du compilateur. Quant aux détails, quelles sont les techniques d'optimisation ? Il en existe de nombreuses. Voici quelques techniques d'optimisation représentatives : élimination de sousexpression commune, élimination de la vérification des limites du tableau, inlining de méthode, analyse d'échappement, etc.

GC

Enfin, on dit que le programme entre dans la phase de la mort. Comment la JVM détermine-t-elle les pilules du programme ? Cet endroit utilise en fait un algorithme d'analyse d'accessibilité. L'idée de base de cet algorithme est d'utiliser une série d'objets appelés « GC Roots » comme point de départ, et de rechercher vers le bas à partir de ce nœud. appelé chaîne de référence, lorsqu'il n'y a pas de chaîne de référence reliant un objet aux racines GC (en termes de théorie des graphes, cela signifie que l'objet est inaccessible depuis les racines GC), cela prouve que l'objet n'est pas disponible et il est déterminé comme étant une chaîne de référence. un objet recyclable. Quand déclenche-t-on la collecte des déchets alors que l’on connaît déjà les objets à recycler ? Les points de sécurité sont des endroits où le programme est temporairement exécuté pour effectuer le GC. À partir de là, nous pouvons facilement savoir que le temps de pause du GC est au cœur du garbage collection. Tous les algorithmes de garbage collection et les garbage collector dérivés sont tous centrés sur la minimisation des temps de pause du GC. Désormais, le dernier garbage collector G1 peut établir un modèle de temps de pause prévisible et planifier pour éviter des opérations complètes dans l'ensemble du tas Java régional. Lorsque nous avons introduit plus tôt le concept de répartition des zones mémoire, nous parlions de nouvelle génération et d'ancienne génération. Différents garbage collector peuvent agir sur la nouvelle génération ou sur l'ancienne génération, et il n'y a même pas de notion de génération (comme le collecteur G1). ). ), cela dit, ce qui suit est une introduction détaillée à l'algorithme de collecte des ordures et au garbage collector correspondant

Algorithme de marquage

L'algorithme de collecte le plus basique, l'algorithme est divisé en deux types : marquer et effacer Étape : Marquez d'abord tous les objets à recycler Une fois le marquage terminé, tous les objets marqués seront recyclés de manière uniforme. Son plus gros défaut est qu'il n'est pas efficace et produit un grand nombre de fragments de mémoire discontinus. Cela provoque des problèmes lorsqu'un objet volumineux est alloué pendant l'exécution du programme, même s'il y a suffisamment de mémoire dans le tas, il ne peut pas trouver suffisamment de mémoire continue. devez déclencher une opération GC. Le garbage collector correspondant ici est le collecteur CMS.

Algorithme de copie

L'algorithme de copie est né pour résoudre les problèmes d'efficacité. Il peut diviser la capacité de mémoire disponible en deux blocs de taille égale et n'en utiliser qu'un à la fois. Lorsque la mémoire est épuisée, copiez les objets survivants dans un autre bloc, puis nettoyez immédiatement l'espace mémoire utilisé. De cette façon, la GC sera effectuée à chaque fois sur toute la demi-zone et des problèmes tels que la fragmentation de la mémoire ne se produiront pas. La plupart des machines virtuelles commerciales actuelles utilisent cet algorithme pour recycler la nouvelle génération. De plus, le rapport de division de la mémoire n'est pas de 1:1. Par exemple, le rapport de taille par défaut d'Eden (une zone Eden) et de Survivor (deux zones Survivor). HotSpot est de 8:1. Chaque fois qu'Eden et l'une des zones Survivant sont utilisés, c'est-à-dire que l'espace mémoire disponible dans la nouvelle génération représente 90 % de l'ensemble de la nouvelle génération. Lors du recyclage, copiez les objets survivants dans Eden et l'un des. les survivants à un autre survivant en même temps. Enfin, nettoyez Eden et l'espace survivant qui vient d'être utilisé. Les lecteurs attentifs peuvent découvrir ici, que se passe-t-il si l'espace survivant inutilisé pendant le processus de copie n'est pas suffisant ? À ce stade, vous devez vous fier à l'ancienne génération pour la garantie d'allocation. Si la garantie réussit, Eden et l'un des objets survivants du Survivant seront déplacés vers l'ancienne génération. Si la garantie échoue, un garbage collection sera effectué. à déclencher chez l’ancienne génération. Pour étendre ce point, le garbage collection de nouvelle génération est appelé Minor GC. Parce que la plupart des objets Java naissent et meurent, Minor GC est très fréquent et la vitesse de récupération est généralement rapide. L'ancienne génération de garbage collection est appelée Major GC/Full GC. Major La vitesse du GC est généralement beaucoup plus lente que celle du Minor GC. Du processus d'analyse précédent, nous pouvons facilement déduire que l'apparition d'un Major GC est souvent accompagnée d'un Minor GC, mais ce n'est donc pas absolu. notre GC consiste en fait à ajuster la vitesse du GC. Il est préférable de contrôler et de réduire autant que possible la fréquence du Major GC. Les ramasse-miettes correspondants ici sont le collecteur Serial, le collecteur ParNew (une version multithread du collecteur Serial, qui peut fonctionner avec le CMS collecteur d'ancienne génération mentionné plus loin) et le collecteur Parallel Scavenge.

 Algorithme de marquage-collation

Cet algorithme est appliqué à l'algorithme de récupération de place de l'ancienne génération, car l'ancienne génération n'est pas recyclée aussi fréquemment que l'algorithme de copie et gaspille également de l'espace. Le processus de marquage-organisation est similaire au marquage-effacement, sauf que les étapes suivantes ne consistent pas à effacer directement les objets recyclables, mais à déplacer tous les objets survivants vers une extrémité, puis à nettoyer directement la mémoire en dehors de la limite d'extrémité. Les garbage collector correspondants ici sont Serial Old Collector et Parallel Old Collector.

Algorithme de collecte générationnelle

Les machines virtuelles commerciales actuelles utilisent toutes cet algorithme. Son idée est de diviser la zone de mémoire du tas en générations comme nous l'avons mentionné plus tôt. Les régions utilisent différents algorithmes de garbage collection. La jeune génération utilise l'algorithme de copie et l'ancienne génération utilise l'algorithme de regroupement de marques ou de balayage de marques.

Révision

Après avoir tant parlé auparavant, peut-être avez-vous une idée de l'histoire de la vie du code Java, ou vous ne le comprenez pas très bien. Nous donnons ici un exemple pour revoir l'ensemble. processus. Que vivrons-nous lorsque nous créerons un nouvel objet ? Combiné avec ce qui a été dit précédemment, lorsque la JVM rencontre une nouvelle instruction, elle vérifie d'abord si l'ensemble du paramètre d'instruction peut localiser une référence de symbole d'une classe dans le pool constant de la zone de méthode, et vérifie si la classe représentée par le symbole entier la référence a été chargée, analysée et initialisée, sinon, le processus de chargement de classe correspondant doit être exécuté en premier. Une fois la vérification du chargement de la classe réussie, la JVM allouera ensuite de la mémoire pour le nouvel objet. Ce processus est effectué dans le tas. La taille d'allocation peut être déterminée une fois le chargement de la classe terminé. Si la mémoire du tas est normale, le pointeur est utilisé. pour déplacer la taille de l'objet. Une distance égale suffit. Cette méthode d'allocation est appelée "collision de pointeur". S'il est dispersé, la JVM maintient une liste pour enregistrer la mémoire disponible, alloue et met à jour les enregistrements de la liste, cette méthode est appelée " free list", quant à la méthode utilisée, dépend du garbage collector utilisé pour le tas que nous avons mentionné plus tôt. Après avoir divisé la mémoire de l'objet, la machine virtuelle effectue les opérations d'initialisation nécessaires. Ensuite, les paramètres nécessaires doivent être définis pour l'objet. Ces informations sont définies dans l'en-tête de l'objet (informations sur les métadonnées de la classe, code de hachage de l'objet, âge de génération du GC de l'objet, etc. . ), une fois ces tâches terminées, un nouvel objet est généré. Ce n'est en fait pas encore terminé. L'étape suivante consiste à appeler la méthode () pour effectuer l'affectation des champs d'objet prévue par le programmeur, et enfin définir la pile La référence pointe vers l'adresse mémoire où se trouve l'objet dans le tas (référence directe). A ce moment, un objet vraiment utilisable a été généré Quant aux différentes opérations ultérieures sur l'objet et sa mort finale, c'est le moteur d'exécution de bytecode évoqué plus haut. Ah GC, je crois que tout le monde ne le connaît plus.

 

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn