Dissiper les mythes sur la gestion de la mémoire dans la JVM



Dans une série d'articles, je souhaite réfuter les idées fausses associées à la gestion de la mémoire et approfondir sa structure dans certains langages de programmation modernes - Java, Kotlin, Scala, Groovy et Clojure. Espérons que cet article vous aidera à comprendre ce qui se passe sous le capot de ces langues. Tout d'abord, nous examinerons la gestion de la mémoire dans la machine virtuelle Java (JVM) , qui est utilisée dans Java, Kotlin, Scala, Clojure, Groovy et d'autres langages. Dans le premier article, j'ai également couvert la différence entre une pile et un tas, ce qui est utile pour comprendre cet article.



Structure de la mémoire JVM



Examinons d'abord la structure de la mémoire JVM. Cette structure est utilisée depuis le JDK 11 . Voici la mémoire disponible pour le processus JVM, elle est allouée par le système d'exploitation:





Il s'agit de la mémoire native allouée par le système d'exploitation et sa taille dépend du système, du processeur et du JRE. À quels domaines et à quoi sont-ils destinés?



Tas



C'est là que la JVM stocke les objets et les données dynamiques. C'est la plus grande zone de mémoire et c'est là que le garbage collector fonctionne. La taille du tas peut être contrôlée avec les indicateurs Xms



(taille initiale) et Xmx



(taille maximale). Le tas n'est pas transféré vers la machine virtuelle dans son ensemble, une partie est réservée en tant qu'espace virtuel, grâce auquel le tas peut croître à l'avenir. Le tas est divisé en espaces de la «jeune» et de la «vieille» génération.



  • La jeune gĂ©nĂ©ration , ou «nouvel espace»: l'espace dans lequel vivent de nouveaux objets. Il est divisĂ© en Eden Space et Survivor Space. La zone de contrĂ´le de la jeune gĂ©nĂ©ration, " le jeune garbage collector " (Minor GC), qui est aussi appelĂ© "le jeune" (Young GC).

    • Paradis : C'est lĂ  que la mĂ©moire est allouĂ©e lorsque nous crĂ©ons de nouveaux objets.
    • Zone de survivant : c'est lĂ  que les objets laissĂ©s par le ramasse-miettes mineur sont stockĂ©s. La zone est divisĂ©e en deux moitiĂ©s, S0 et S1 .
  • Ancienne gĂ©nĂ©ration , ou «stockage» (Tenured Space): Cela inclut les objets qui ont atteint le seuil de stockage maximal au cours de la vie d'un garbage collector junior. Cet espace est gĂ©rĂ© par un GC majeur.


Piles de fils



Il s'agit d'une zone de pile dans laquelle une pile est allouée par thread. C'est là que les données statiques spécifiques aux threads sont stockées, y compris les cadres de méthode et de fonction, et les pointeurs vers des objets. La taille de la mémoire de la pile peut être définie à l'aide d'un indicateur Xss



.



Metaspace



Cela fait partie de la mémoire native, par défaut, il n'a pas de limite supérieure. Dans les versions antérieures de la JVM, cette mémoire est appelée espace de génération permanent ( espace permanent de génération (PermGen)) . Les chargeurs de classe y stockaient des définitions de classe. Si cet espace augmente, le système d'exploitation peut déplacer les données stockées ici de la RAM vers la mémoire virtuelle, ce qui peut ralentir l'application. Cela peut être évité en définissant la taille de MetaSpace via des indicateurs XX:MetaspaceSize



et -XX:MaxMetaspaceSize



, dans ce cas, l'application peut émettre une erreur de mémoire.



Cache de code



C'est là que le compilateur Just In Time (JIT) stocke les blocs de code compilés auxquels vous devez accéder fréquemment. Habituellement, la JVM interprète le bytecode en code machine natif, mais le code compilé par le compilateur JIT n'a pas besoin d'être interprété, il est déjà au format natif et mis en cache dans cette zone de mémoire.



Bibliothèques partagées



C'est là que le code natif de toutes les bibliothèques partagées est stocké. Cette zone de mémoire est chargée par le système d'exploitation une seule fois pour chaque processus.



Utilisation de la mémoire JVM: pile et tas



Voyons maintenant comment le programme exécutable utilise les parties les plus importantes de la mémoire. Utilisons le code ci-dessous. Il n'est pas optimisé pour l'exactitude, donc ignorez les problèmes tels que les variables intermédiaires inutiles, les modificateurs incorrects, etc. Son travail est de visualiser l'utilisation de la pile et du tas.



class Employee {
    String name;
    Integer salary;
    Integer sales;
    Integer bonus;

    public Employee(String name, Integer salary, Integer sales) {
        this.name = name;
        this.salary = salary;
        this.sales = sales;
    }
}

public class Test {
    static int BONUS_PERCENTAGE = 10;

    static int getBonusPercentage(int salary) {
        int percentage = salary * BONUS_PERCENTAGE / 100;
        return percentage;
    }

    static int findEmployeeBonus(int salary, int noOfSales) {
        int bonusPercentage = getBonusPercentage(salary);
        int bonus = bonusPercentage * noOfSales;
        return bonus;
    }

    public static void main(String[] args) {
        Employee john = new Employee("John", 5000, 5);
        john.bonus = findEmployeeBonus(john.salary, john.sales);
        System.out.println(john.bonus);
    }
}

      
      





Vous pouvez voir ici comment le programme ci-dessus est exécuté et comment la pile et le tas sont utilisés:



https://files.speakerdeck.com/presentations/9780d352c95f4361bd8c6fa164554afc/JVM_memory_use.pdf



Comme vous pouvez le voir:



  • Chaque appel de fonction est poussĂ© sur le thread de la pile d'exĂ©cution en tant que bloc de trame.
  • Toutes les variables locales, y compris les arguments et les valeurs de retour, sont stockĂ©es sur la pile Ă  l'intĂ©rieur de blocs de cadre de fonction.
  • int .
  • Employee, Integer String , . .
  • , , .
  • , .
  • ().
  • , .


La pile est automatiquement gérée par le système d'exploitation et non par la JVM. Par conséquent, il n'est pas nécessaire de prendre soin de lui. Mais le tas n'est plus géré de cette façon, et comme il s'agit de la plus grande zone de mémoire contenant des données dynamiques, il peut croître de manière exponentielle et le programme peut occuper toute la mémoire au fil du temps. De plus, le tas se fragmente progressivement, ce qui ralentit les performances des applications. La JVM aidera à résoudre ces problèmes. Il gère automatiquement le tas à l'aide du garbage collection.



Gestion de la mémoire JVM: garbage collection



Jetons un coup d'œil à la gestion automatique du tas, qui joue un rôle très important dans les performances des applications. Lorsqu'un programme tente d'allouer plus de mémoire sur le tas que ce qui est disponible (selon la valeur Xmx



), nous sortons des erreurs de mémoire .



La machine virtuelle Java gère le tas à l'aide du garbage collection. Pour faire de la place à la création d'un nouvel objet, la JVM nettoie la mémoire occupée par des objets orphelins, c'est-à-dire des objets qui ne sont plus référencés directement ou indirectement à partir de la pile.





Le garbage collector JVM est responsable de:



  • RĂ©cupĂ©ration de la mĂ©moire du système d'exploitation et retour au système d'exploitation.
  • Transfert de la mĂ©moire allouĂ©e Ă  l'application Ă  sa demande.
  • DĂ©terminez quelles parties de la mĂ©moire allouĂ©e sont encore utilisĂ©es par l'application.
  • RĂ©clamation de la mĂ©moire inutilisĂ©e Ă  utiliser par l'application.


Les garbage collector de la JVM fonctionnent sur une base générationnelle (les objets du tas sont regroupés par âge et nettoyés à différentes étapes). Il existe de nombreux algorithmes de récupération de place, mais Mark & ​​Sweep est le plus couramment utilisé .



Garbage collector Mark & ​​Sweep



La machine virtuelle Java utilise un thread démon distinct qui s'exécute en arrière-plan pour le garbage collection. Ce processus démarre lorsque certaines conditions sont remplies. Le collecteur Mark & ​​Sweep fonctionne généralement en deux étapes, parfois une troisième est ajoutée, en fonction de l'algorithme utilisé.





  • Balisage : tout d'abord, le collecteur dĂ©termine quels objets sont utilisĂ©s et lesquels ne le sont pas. Ceux utilisĂ©s ou accessibles par les pointeurs de pile sont marquĂ©s rĂ©cursivement comme vivants.
  • Suppression : le collecteur parcourt le tas et supprime tous les objets qui ne sont pas marquĂ©s comme vivants. Ces emplacements de mĂ©moire sont marquĂ©s comme libres.
  • Compression : après avoir supprimĂ© les objets inutilisĂ©s, tous les objets survivants sont dĂ©placĂ©s de manière Ă  ĂŞtre ensemble. Cela rĂ©duit la fragmentation et accĂ©lère l'allocation de mĂ©moire pour les nouveaux objets.


Ce type de collecteur est également appelé stop-the-world, car lorsqu'ils sont supprimés, il y a des pauses dans l'application.



La JVM propose plusieurs algorithmes de récupération de place parmi lesquels choisir, et en fonction de votre JDK, il peut y avoir encore plus d'options (par exemple, le collecteur Shenandoah dans OpenJDK). Les auteurs de différentes implémentations visent différents objectifs:



  • DĂ©bit : temps passĂ© sur le garbage collection, sans exĂ©cuter l'application. IdĂ©alement, le dĂ©bit doit ĂŞtre Ă©levĂ©, c'est-Ă -dire que les pauses de rĂ©cupĂ©ration de place sont courtes.
  • DurĂ©e des pauses : durĂ©e pendant laquelle le garbage collector interfère avec l'exĂ©cution de l'application. IdĂ©alement, les pauses devraient ĂŞtre très courtes.
  • Taille du tas : devrait idĂ©alement ĂŞtre petite.


Collectionneurs dans JDK 11



JDK 11 est la version LTE actuelle. Vous trouverez ci-dessous une liste des garbage collector disponibles, et la JVM en choisit un par défaut en fonction du matériel et du système d'exploitation actuels. Nous pouvons toujours forcer un sélecteur à être sélectionné à l'aide d'un bouton radio -XX



.



  • : , , . -XX:+UseSerialGC



    .
  • : , . , / . -XX:+UseParallelGC



    .
  • Garbage-First (G1): ( ). , . . -XX:+UseG1GC



    .
  • Z: , , JDK11. . , stop-the-world. , / ( ). -XX:+UseZGC



    .




Quel que soit le collecteur sélectionné, la JVM utilise deux types d'assemblage: le collecteur junior et le collecteur senior.



Assembleur junior



Il maintient la propreté et la compacité de l'espace de la jeune génération. Il est lancé lorsque la JVM ne parvient pas à obtenir la mémoire nécessaire au paradis pour accueillir un nouvel objet. Au départ, toutes les zones du tas sont vides. Le paradis se remplit en premier, suivi de la zone des survivants et à la fin du stockage.



Vous pouvez voir le processus de ce collecteur ici:



https://files.speakerdeck.com/presentations/f4783404769145f4b990154d0cc05629/JVM_minor_GC.pdf



  1. Disons qu'il y a déjà des objets au paradis (les blocs 01 à 06 sont marqués comme étant utilisés).
  2. L'application crée un nouvel objet (07).
  3. JVM , , JVM .
  4. ( ), — ().
  5. JVM S0 S1 «» (To Space), S0. «» , , , .
  6. , .
  7. , - , ( 07 13 ).
  8. (14).
  9. JVM , , JVM .
  10. , , « ».
  11. JVM «» S1, S0 «». «» «» (S1), , . , «», , (premature promotion). , .
  12. «» (S0), .
  13. Ceci est répété à chaque session de collectionneur junior, les survivants se déplacent entre S0 et S1, et leur âge augmente. Lorsqu'il atteint le "seuil maximum" spécifié, qui est de 15 par défaut, l'objet est déplacé vers le "stockage".


Nous avons examiné comment le collectionneur junior nettoie la mémoire dans l'espace de la jeune génération. Il s'agit d'un processus d'arrêt du monde, mais il est si rapide que sa durée peut généralement être négligée.



Assembleur senior



Surveille la propreté et la compacité de l'espace de l'ancienne génération (stockage). Fonctionne dans l'une des conditions suivantes:



  • Le dĂ©veloppeur appelle le programme System



    . gc()



    ou Runtime.getRunTime().gc()



    .
  • La JVM dĂ©cide que le magasin est Ă  court de mĂ©moire car il est plein Ă  la suite des sessions prĂ©cĂ©dentes du collecteur junior.
  • Si lors de l'exĂ©cution de la JVM collector junior ne peut pas obtenir suffisamment de mĂ©moire dans le paradis ou dans la zone des survivants.
  • Si nous dĂ©finissons un paramètre dans la JVM MaxMetaspaceSize



    et qu'il n'y a pas assez de mémoire pour charger de nouvelles classes.


Le processus de travail du collectionneur senior est plus simple que celui du junior:



  1. Disons que de nombreuses sessions de collectionneurs juniors sont passées et que le stockage est presque plein. La JVM décide d'exécuter l'ancien collecteur.
  2. Dans le stockage, il parcourt récursivement le graphe d'objets à partir des pointeurs de pile et marque les objets utilisés comme (mémoire utilisée), le reste comme garbage (perdu). Si le collectionneur senior a été lancé pendant le travail du collectionneur junior, alors son travail couvre l'espace de la jeune génération (le paradis et le domaine des survivants) et la voûte.
  3. Le collecteur supprime tous les objets orphelins et récupère la mémoire.
  4. S'il ne reste aucun objet sur le tas pendant le travail de l'ancien collecteur, la JVM récupère également la mémoire de la métaspace, en supprimant les classes chargées, s'il s'agit d'un garbage collection complet.


Conclusion



Nous avons couvert la structure et la gestion de la mémoire de la JVM. Cet article n'est pas exhaustif, nous n'avons pas parlé de nombreux concepts et moyens de personnalisation les plus complexes pour des cas d'utilisation spécifiques. Vous pouvez lire plus de détails ici .



Mais pour la plupart des développeurs JVM (Java, Kotlin, Scala, Clojure, JRuby, Jython), cette quantité d'informations sera suffisante. J'espère que vous pouvez maintenant écrire un meilleur code, créer des applications plus efficaces, en évitant divers problèmes de fuites de mémoire.



Liens






All Articles