Collecteur de déchets fait maison pour OpenJDK

Il s'agit d'une traduction de l'article d'Alexey Shipilev «Faites-le vous-même (OpenJDK) Garbage Collector» , publié avec le consentement de l'auteur. Signalez toutes les fautes de frappe et autres bogues dans PM - nous les corrigerons.

Le processus de création de quelque chose lors de l'exécution est un exercice amusant. Au moins la création de la première version! Construire un sous-système d'exécution fiable, performant et à sécurité intégrée, dont le comportement peut être facilement observé et débogué, est une tâche très, très difficile.


Faire un simple garbage collector est d'une simplicité trompeuse, et maintenant je veux le faire dans cet article. Roman Kennke au FOSDEM 2019 a fait une présentation et une démo intitulée «Écrire un GC en 20 minutes» en utilisant une version antérieure de ce patch. Malgré le fait que le code implémenté ici en démontre beaucoup et soit abondamment commenté, il est nécessaire d'avoir une bonne description de haut niveau de ce qui se passe - c'est ainsi que cet article est apparu.


Une compréhension de base du travail des ramasseurs d'ordures aidera grandement à comprendre ce qui est écrit ici. L'article utilisera des détails et des idées dans une implémentation spécifique de HotSpot, mais il n'y aura pas de cours d'introduction à la conception de GC ici. Prenez le manuel du GC et lisez les premiers chapitres sur les bases du GC, et encore plus vite commencera l'article Wikipedia .



Table des matières




1. En quoi consiste GC


Maintenant que de nombreux GC différents ont été écrits, il est assez simple de créer les vôtres - de nombreux éléments déjà écrits peuvent être (ré) utilisés pour transférer certaines des inquiétudes concernant les détails d'implémentation vers du code éprouvé et testé.



1.1. Epsilon gc


OpenJDK 11 présente un nouveau JEP 318: "Epsilon: un garbage collector sans opération (expérimental)" . Sa tâche est de fournir une implémentation minimale pour le cas où la libération de mémoire n'est pas nécessaire ni même interdite. JEP explique plus en détail pourquoi cela pourrait être utile.


Du point de vue de l'implémentation, «garbage collector» est un mauvais nom, il serait plus correct d'utiliser le terme «gestionnaire de mémoire automatique» , qui est responsable de l'allocation et de la libération de mémoire. Epsilon GC n'implémente que «l'allocation» et ne traite pas du tout de la «libération». Par conséquent, vous pouvez prendre Epsilon GC et commencer à implémenter les algorithmes de «libération» à partir de zéro.



1.1.1. Allocation de mémoire


La partie la plus développée du GC Epsilon est responsable de l'allocation de mémoire . Il sert des demandes externes pour allouer de la mémoire de taille arbitraire et créer un tampon d'allocation de thread local (TLAB) de la taille souhaitée. L'implémentation elle-même essaie de ne pas trop étendre le TLAB, car il n'y aura pas de mémoire libre et personne ne renverra les octets perdus.



1.1.2. Obstacles


Certains garbage collector nécessitent une interaction avec l'application pour maintenir les invariants GC, forçant le runtime et l'application à créer des soi-disant barrières lorsqu'ils tentent d'accéder au tas. Cela est vrai pour tous les collectionneurs multi-threads, ainsi que pour de nombreux collectionneurs de générations et d'arrêt du monde.


Epsilon ne nécessite pas de barrières, mais le runtime et le compilateur veulent toujours savoir que les barrières ne font rien. Le manipuler à chaque fois partout peut être fatigant. Heureusement, à partir d'OpenJDK 11, il existe un nouveau JEP-304: «Interface de collecte des ordures» , ce qui facilite considérablement l'insertion des barrières. En particulier, la barrière définie dans Epsilon est vide et tout le travail trivial - sauvegarde, chargement, CAS, copie sur tableau - peut être délégué aux implémentations de barrières triviales à partir d'une superclasse existante. Si vous créez un GC qui n'a pas non plus besoin de barrières, vous pouvez simplement réutiliser le code d'Epsilon.



1.1.3. Surveillance de la connexion


La dernière partie fastidieuse de la mise en œuvre du GC est liée à un tas de mécanismes de surveillance à l'intérieur de la JVM: les bacs MX, les commandes de diagnostic, etc. devraient fonctionner. Epsilon a déjà fait tout cela pour vous.



1.2. Rantime et GC



1.2.1. Éléments racine


Le garbage collector, dans le cas général, a besoin de savoir exactement ce qui dans le runtime Java a des références de tas. Ces éléments racine, appelés racines GC , peuvent être des emplacements sur des piles de flux et des variables locales (y compris celles trouvées dans le code compilé JIT!), Des classes natives et des chargeurs de classe, des références dans JNI, etc. Les tentatives d'identification de ces éléments peuvent être très complexes et fastidieuses. Mais dans Hotspot, ils sont tous suivis à l'aide des sous-systèmes VM appropriés, vous pouvez donc simplement apprendre comment les implémentations GC existantes fonctionnent avec eux. Plus loin dans le texte, nous le verrons.



1.2.2. Exploration d'objets


Le garbage collector doit contourner les liens sortants dans les objets Java. Cette opération se trouve partout, de sorte que les parties communes de l'exécution fournissent des solutions de contournement toutes faites; vous n'avez pas besoin d'écrire quoi que ce soit vous-même. Ci-dessous, il y aura une section avec une implémentation spécifique, et vous y trouverez, par exemple, les appels obj→oop_iterate .



1.2.3. Déplacements


Le ramasse-miettes en mouvement doit noter quelque part les nouvelles adresses des objets déplacés. Il existe plusieurs endroits où vous pouvez écrire ces données de transfert .


  1. Vous pouvez réutiliser le «mot marqueur» dans l'objet lui-même (série, parallèle, etc.). Une fois le monde arrêté, tous les accès à l'objet sont contrôlés et il est garanti qu'aucun thread Java ne peut voir les données temporaires que nous avons décidé d'entrer dans le mot marqueur. Vous pouvez le réutiliser pour stocker des données de transfert.
  2. Vous pouvez gérer une table de mouvement native distincte ( ZGC , C4 et autres). Cela isole complètement le GC du runtime et du reste de l'application, car seul le GC connaît l'existence d'une telle table. Les assembleurs compétitifs utilisent généralement un tel schéma - ils ne veulent pas souffrir d'un tas de problèmes inutiles.
  3. Vous pouvez ajouter un autre mot à l'objet ( Shenandoah et autres). Cette combinaison des deux approches précédentes permet non seulement au runtime et à l'application de fonctionner sans problème avec les en-têtes existants, mais enregistre également les données de transfert.


1.2.4. Données de marqueur


Le garbage collector doit écrire des données de marquage quelque part. Et encore une fois, il existe plusieurs façons de les enregistrer:


  1. Vous pouvez réutiliser le mot marqueur dans l'objet lui-même (série, parallèle, etc.). Encore une fois, en mode d'arrêt mondial, vous pouvez utiliser les bits du mot marqueur pour coder le fait d'une étiquette. De plus, si vous avez besoin de faire le tour de tous les objets vivants, nous suivons le tas, objet après objet - cela est possible car le tas est analysable .
  2. Vous pouvez gérer une structure distincte pour le stockage des données de marquage (G1, Shenandoah, etc.). Cela se fait généralement à l'aide d'un bitmap distinct , qui mappe tous les N octets du tas sur 1 bit de la carte. Habituellement, les objets Java sont alignés sur 8 octets , de sorte que la carte mappe tous les 64 bits du tas à 1 bit de la carte, occupant 1/64 de la taille du tas dans la mémoire native. Ces frais généraux sont payants lors de l'analyse du tas pour la présence d'objets vivants, en particulier clairsemés: le contournement de la carte est souvent beaucoup plus rapide que le contournement du tas désassemblé objet par objet.
  3. Encodez les étiquettes en liens eux-mêmes (ZGC, C4 et autres). Cela nécessite une coordination avec l'application, puis vous devez couper toutes ces étiquettes des liens ou effectuer d'autres astuces pour maintenir l'exactitude. En d'autres termes, nous avons besoin de barrières ou de travaux supplémentaires de la part du GC.


2. Plan général


Très probablement, le plus facile à implémenter au-dessus d'Epsilon est le Mark-Compact, dans le style LISP2. L'idée de base de ce GC est décrite à la fois sur Wikipedia et dans le Manuel du GC (chapitre 3.2). Un croquis de l'algorithme sera dans la section avec l'implémentation ci-dessous, mais je recommande fortement de lire un peu Wikipedia ou le manuel du GC pour comprendre ce que nous allons faire.


L'algorithme en question est le GC décalé : les objets en mouvement se déplacent dans un tableau au tout début du tas. Il a ses avantages et ses inconvénients:


  • Il maintient l'ordre des allocations de mémoire. C'est très bien pour contrôler la mise en page en mémoire, si cela vous intéresse (contrôlez les monstres, c'est votre temps!). L'inconvénient est que vous n'obtiendrez pas la localité de lien automatique de cette façon.
  • Sa complexité est O (N) du nombre d'objets. Cependant, la linéarité a un prix: GC doit contourner un tas de 4 fois pour chaque cycle de construction.
  • Il ne nécessite pas de mémoire libre sur le tas! Il n'est pas nécessaire de réserver de la mémoire sur le tas pour évacuer les objets vivants, vous pouvez donc même travailler avec un tas débordé de 99. (9)%. Si nous adoptons d'autres idées de simples collectionneurs, par exemple un charognard avec un semi-espace (charognard semi-spatial), nous devrons réécrire légèrement la présentation du tas et réserver un peu d'espace pour l'évacuation, mais cela dépasse le cadre de cet exercice.
  • Si vous travaillez un peu sur le problème, vous pouvez atteindre zéro consommation de mémoire et de temps pendant les périodes où le CPG est inactif. Il démarre sur une mémoire dans un état arbitraire et s'arrête, le compactant de manière significative. Cela correspond très bien au fonctionnement d'Epsilon: il ne fait que surligner juste après le dernier objet. C'est aussi un inconvénient: quelques objets morts au début du tas entraînent un grand nombre de mouvements.
  • Il ne nécessite tout simplement pas de nouvelles barrières, vous pouvez réutiliser EpsilonBarrierSet tel EpsilonBarrierSet .

Par souci de simplicité, l'implémentation GC utilisera un arrêt complet du monde (stop-the-world, STW), elle n'aura pas de générations ni de multithreading. Dans ce cas, il est judicieux d'utiliser une image bitmap pour stocker les marques et réutiliser le mot marqueur pour stocker les données de mouvement.



3. Mise en œuvre du noyau du GC


Lire et comprendre toute la mise en œuvre peut être trop compliqué pour une personne ignorante. Dans cette section, nous allons le comprendre étape par étape.



3.1. Prologue


Le ramasse-miettes doit généralement faire deux ou trois choses pour préparer la collecte. Lisez les commentaires, ils devraient parler d'eux-mêmes:


 { GCTraceTime(Info, gc) time("Step 0: Prologue", NULL); //      .      //   :   ,   ,  // «»   ,      //   ,     . if (!os::commit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size(), false)) { log_warning(gc)("Could not commit native memory for marking bitmap, GC failed"); return; } //        ,  , //       TLAB-. ensure_parsability(true); //      ,    GC. CodeCache::gc_prologue(); BiasedLocking::preserve_marks(); //        . //       . DerivedPointerTable::clear(); } 

Étant donné que nous utilisons un bitmap pour suivre l'accessibilité des objets, nous devons l'effacer avant utilisation. Ou dans notre cas, puisque nous visons à ne jamais demander de ressources avant de démarrer le cycle GC, nous devrons valider la bitmap en mémoire à l' avance. Cela offre plusieurs avantages intéressants, au moins sous Linux, où la plupart du bitmap pointera vers la page zéro, en particulier pour les tas clairsemés.


Les threads doivent libérer leurs TLAB et en demander de nouveaux au GC une fois la construction terminée.


Ne confondez pas TLAB et java.lang.ThreadLocal . Du point de vue du GC, ThreadLocal sont des objets ordinaires, et ils ne seront pas compilés par le GC sauf indication contraire dans le code Java.

Certaines parties de l'exécution, en particulier celles qui contiennent des liens vers le tas Java, se briseront lors de la récupération de place, vous devez donc les avertir spécifiquement que le GC commencera bientôt à fonctionner. Cela permettra aux sous-systèmes respectifs de préparer et d'enregistrer une partie de leur état avant que le GC ne se déplace.



3.2. Marquage


Marquer en mode stop du monde devient assez simple quand presque tout a déjà été fait pour nous. L'étiquetage est assez standard, et très probablement, dans de nombreuses implémentations, GC est la première étape.


 { GCTraceTime(Info, gc) time("Step 1: Mark", NULL); //    ,     .  //   ,  ,    //      . EpsilonMarkStack stack; EpsilonScanOopClosure cl(&stack, &_bitmap); //      . process_roots(&cl); stat_reachable_roots = stack.size(); //    ,    . //    ,   , //      . while (!stack.is_empty()) { oop obj = stack.pop(); obj->oop_iterate(&cl); stat_reachable_heap++; } //       . DerivedPointerTable::set_active(false); } 

Cela fonctionne exactement de la même manière que pour tout autre graphique: vous commencez la traversée avec l'ensemble initial de sommets accessibles, longez les bords sortants et enregistrez tous les sommets visités. La traversée continue jusqu'à la fin de tous les pics non visités. Dans GC, les «sommets» sont des objets et les «arêtes» sont des liens entre eux.


Techniquement, nous pourrions simplement parcourir récursivement le graphe des objets, mais c'est une mauvaise idée pour les graphes arbitraires qui peuvent avoir de très grands diamètres. Imaginez une liste chaînée d'un milliard de pics! Par conséquent, pour limiter la profondeur de récursivité, nous utilisons une pile de marquage qui enregistre les objets détectés.


L'ensemble initial d'objets accessibles est les racines GC. Maintenant, ne vous attardez pas sur ce qu'est process_roots , plus sur cela plus tard. Pour l'instant, disons simplement qu'il contourne tous les liens accessibles du côté VM.


Une image bitmap avec des marques sert à la fois d'outil pour enregistrer le front d'onde de marquage (beaucoup d'objets déjà visités) et au final - comme référentiel du résultat souhaité, un ensemble de tous les objets accessibles. Le vrai travail se déroule dans EpsilonScanOopClosure , il est appliqué à tous les objets intéressants et itéré sur tous les liens de l'objet sélectionné.


Regardez, Java a su fermer (fermer) avant de devenir à la mode!

 class EpsilonScanOopClosure : public BasicOopIterateClosure { private: EpsilonMarkStack* const _stack; MarkBitMap* const _bitmap; template <class T> void do_oop_work(T* p) { // p -     ,   oop,   //      ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //  . ,   .  , //        . //    +, //       . if (!_bitmap->is_marked(obj)) { _bitmap->mark((HeapWord*)obj); _stack->push(obj); } } } }; 

Une fois cette étape _bitmap , _bitmap contient des bits indiquant l'emplacement des objets _bitmap . Grâce à cela, il est possible de contourner tous les objets vivants, par exemple:


 //           . //   ,    ( )  ,  //       1/64  . void EpsilonHeap::walk_bitmap(ObjectClosure* cl) { HeapWord* limit = _space->top(); HeapWord* addr = _bitmap.get_next_marked_addr(_space->bottom(), limit); while (addr < limit) { oop obj = oop(addr); assert(_bitmap.is_marked(obj), "sanity"); cl->do_object(obj); addr += 1; if (addr < limit) { addr = _bitmap.get_next_marked_addr(addr, limit); } } } 


3.3. Calculer de nouvelles adresses


C'est également une étape assez simple, et elle met en œuvre exactement ce que dit l'algorithme.



 //    forwarding data (,    ) //   .        . //          . PreservedMarks preserved_marks; //     GC. HeapWord* new_top; { GCTraceTime(Info, gc) time("Step 2: Calculate new locations", NULL); //    ,        //    . ,  - . EpsilonCalcNewLocationObjectClosure cl(_space->bottom(), &preserved_marks); walk_bitmap(&cl); //         . //       ,    //      ,      "" //  . new_top = cl.compact_point(); stat_preserved_marks = preserved_marks.size(); } 

La seule chose qui attire votre attention, c'est que nous avons décidé de stocker de nouvelles adresses dans le mot de marquage des objets Java, et ce mot peut déjà être occupé par quelque chose d'important, par exemple, des informations sur les verrous. Heureusement, de tels mots de marquage non triviaux sont assez rares, et nous pouvons simplement les stocker séparément, si cela est nécessaire: c'est pour cela que PreservedMarks utilisé.


Le vrai travail algorithmique est effectué par EpsilonCalcNewLocationObjectClosure :


 class EpsilonCalcNewLocationObjectClosure : public ObjectClosure { private: HeapWord* _compact_point; PreservedMarks* const _preserved_marks; public: EpsilonCalcNewLocationObjectClosure(HeapWord* start, PreservedMarks* pm) : _compact_point(start), _preserved_marks(pm) {} void do_object(oop obj) { //    :    . //        (      , //    ),      , //     . if ((HeapWord*)obj != _compact_point) { markOop mark = obj->mark_raw(); if (mark->must_be_preserved(obj)) { _preserved_marks->push(obj, mark); } obj->forward_to(oop(_compact_point)); } _compact_point += obj->size(); } HeapWord* compact_point() { return _compact_point; } }; 

forward_to est la partie la plus importante car elle stocke "l'adresse de déplacement" dans le mot marqueur de l'objet. Cela sera nécessaire dans les prochaines étapes.



3.4. Correction des pointeurs


Vous devez maintenant parcourir à nouveau le tas et réécrire tous les liens avec leurs nouvelles adresses selon l'algorithme suivant:



 { GCTraceTime(Info, gc) time("Step 3: Adjust pointers", NULL); //     _   _,     // « ».      forwarding data, //    .      . EpsilonAdjustPointersObjectClosure cl; walk_bitmap(&cl); //     ,      VM,  //     :      . EpsilonAdjustPointersOopClosure cli; process_roots(&cli); //   ,      , //     . preserved_marks.adjust_during_full_gc(); } 

Il existe deux types de références aux objets déplacés: sortant soit des objets sur le tas lui-même, soit des racines GC. Vous devez mettre à jour les deux classes de liens. Certaines étiquettes enregistrées stockent également des références aux objets, vous devez donc leur demander de mettre à jour. PreservedMarks sait comment faire cela car il attend des «données de transfert» au même endroit où nous les avons enregistrées, dans le mot de marquage de l'objet.


Les fermetures sont divisées en deux types: certaines prennent des objets et contournent leur contenu, d'autres mettent à jour ces adresses. Ici, vous pouvez faire une petite optimisation des performances: si l'objet ne bouge pas, vous pouvez enregistrer quelques enregistrements dans un groupe:


 class EpsilonAdjustPointersOopClosure : public BasicOopIterateClosure { private: template <class T> void do_oop_work(T* p) { // p -     ,   oop. //        ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //         . //  ,    . if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); RawAccess<>::oop_store(p, fwd); } } } }; class EpsilonAdjustPointersObjectClosure : public ObjectClosure { private: EpsilonAdjustPointersOopClosure _cl; public: void do_object(oop obj) { //    ,    : obj->oop_iterate(&_cl); } }; 

Après avoir terminé cette étape, nous avons essentiellement cassé le tas: les liens pointent vers les «mauvaises» adresses auxquelles les objets ne se trouvent pas encore. Corrigeons-le!



3.5. Nous déplaçons des objets


Temps pour déplacer des objets vers de nouvelles adresses, conformément à l'algorithme:



EpsilonMoveObjectsObjectClosure nouveau le tour des tas et appliquez la fermeture EpsilonMoveObjectsObjectClosure à tous les objets vivants:


 { GCTraceTime(Info, gc) time("Step 4: Move objects", NULL); //       . //          . EpsilonMoveObjectsObjectClosure cl; walk_bitmap(&cl); stat_moved = cl.moved(); //         ,   // «»      . _space->set_top(new_top); } 

Immédiatement après cela, vous pouvez faire glisser le tas du tas de points de compactage, ce qui permet d'allouer de la mémoire directement à partir de cet emplacement, immédiatement après la fin du cycle de récupération de place.


Notez que dans l'assemblage de décalage, nous pouvons écraser le contenu des objets existants, mais comme la numérisation va dans le même sens, les objets écrasés sont déjà copiés au bon endroit.


L'ancien et le nouvel emplacement de la même installation peuvent se croiser. Par exemple, si vous décalez un objet de 100 octets de 8 octets. La procédure de copie doit fonctionner par elle-même, et le contenu se croisant doit être copié correctement, faites attention à Copy::aligned_*conjoint*_words .

La fermeture elle-même déplacera simplement les objets déplacés vers les nouvelles adresses:


 class EpsilonMoveObjectsObjectClosure : public ObjectClosure { public: void do_object(oop obj) { //     ,  .   - , //   -  mark word, //      forwarding data. if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); Copy::aligned_conjoint_words((HeapWord*)obj, (HeapWord*)fwd, obj->size()); fwd->init_mark_raw(); } } }; 


3.6. Épilogue


La collecte des ordures est terminée, le tas est à nouveau presque cohérent, les dernières touches sont laissées:


 { GCTraceTime(Info, gc) time("Step 5: Epilogue", NULL); //    . preserved_marks.restore(); //   ,    . DerivedPointerTable::update_pointers(); BiasedLocking::restore_marks(); CodeCache::gc_epilogue(); JvmtiExport::gc_epilogue(); //     . if (!os::uncommit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size())) { log_warning(gc)("Could not uncommit native memory for marking bitmap"); } //    ,  . //        . if (EpsilonUncommit) { _virtual_space.shrink_by((_space->end() - new_top) * HeapWordSize); _space->set_end((HeapWord*)_virtual_space.high()); } } 

Nous informons le reste du runtime qu'ils doivent démarrer les procédures de post-assemblage. Nous restaurons les mots marqueurs spéciaux que nous avons enregistrés précédemment. Adieu bisous à notre carte marqueur - elle n'est plus nécessaire.


Et, si vous le voulez vraiment, vous pouvez réduire la mémoire pour les allocations à une nouvelle taille, retournant ainsi la mémoire au système d'exploitation!



4. Connectez le GC à la VM



4.1. Traversée radiculaire


Rappelez-vous, vous devez contourner les liens spéciaux accessibles de VM? Vous pouvez demander à chaque sous-système VM spécial de contourner les liens cachés des autres objets Java. Une liste exhaustive de ces éléments racine dans le Hotspot actuel ressemble à ceci:


 void EpsilonHeap::do_roots(OopClosure* cl) { //   ,        1 . StrongRootsScope scope(1); //         . CLDToOopClosure clds(cl, ClassLoaderData::_claim_none); MarkingCodeBlobClosure blobs(cl, CodeBlobToOopClosure::FixRelocations); //      . //        . { MutexLockerEx lock(CodeCache_lock, Mutex::_no_safepoint_check_flag); CodeCache::blobs_do(&blobs); } { MutexLockerEx lock(ClassLoaderDataGraph_lock); ClassLoaderDataGraph::cld_do(&clds); } Universe::oops_do(cl); Management::oops_do(cl); JvmtiExport::oops_do(cl); JNIHandles::oops_do(cl); WeakProcessor::oops_do(cl); ObjectSynchronizer::oops_do(cl); SystemDictionary::oops_do(cl); Threads::possibly_parallel_oops_do(false, cl, &blobs); } 

, . GC .



4.2.


GC , VM . Hotspot VM_Operation , GC VM- :


 // VM_operation,      class VM_EpsilonCollect: public VM_Operation { private: const GCCause::Cause _cause; EpsilonHeap* const _heap; static size_t _last_used; public: VM_EpsilonCollect(GCCause::Cause cause) : VM_Operation(), _cause(cause), _heap(EpsilonHeap::heap()) {}; VM_Operation::VMOp_Type type() const { return VMOp_EpsilonCollect; } const char* name() const { return "Epsilon Collection"; } virtual bool doit_prologue() { //     ,     . //         GC, //          . //   ,         //  .     , //       1%, ,  , //     . Heap_lock->lock(); size_t used = _heap->used(); size_t capacity = _heap->capacity(); size_t allocated = used > _last_used ? used - _last_used : 0; if (_cause != GCCause::_allocation_failure || allocated > capacity / 100) { return true; } else { Heap_lock->unlock(); return false; } } virtual void doit() { _heap->entry_collect(_cause); } virtual void doit_epilogue() { _last_used = _heap->used(); Heap_lock->unlock(); } }; size_t VM_EpsilonCollect::_last_used = 0; void EpsilonHeap::vmentry_collect(GCCause::Cause cause) { VM_EpsilonCollect vmop(cause); VMThread::execute(&vmop); } 

, GC — , .



4.3.


, GC , , GC , . , allocate_work , GC :


 HeapWord* EpsilonHeap::allocate_or_collect_work(size_t size) { HeapWord* res = allocate_work(size); if (res == NULL && EpsilonSlidingGC) { vmentry_collect(GCCause::_allocation_failure); res = allocate_work(size); } return res; } 

!



5.


OpenJDK.


 $ hg clone https://hg.openjdk.java.net/jdk/jdk/ jdk-jdk $ cd jdk-jdk $ curl https://shipilev.net/jvm/diy-gc/webrev/jdk-jdk-epsilon.changeset | patch -p1 

OpenJDK :


 $ ./configure --with-debug-level=fastdebug $ make images 

:


 $ build/linux-x86_64-server-fastdebug/images/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC -version openjdk version "13-internal" 2019-09-17 OpenJDK Runtime Environment (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon) OpenJDK 64-Bit Server VM (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon, mixed mode, sharing 


6.


, GC ? :


  1. . . Hotspot , JVM fastdebug , GC.
  2. . , . , ( ) , .
  3. Tests. , , , . - , .

, , :


 $ CONF=linux-x86_64-server-fastdebug make images run-test TEST=gc/epsilon/ Building targets 'images run-test' in configuration 'linux-x86_64-server-fastdebug' Test selection 'gc/epsilon/', will run: * jtreg:test/hotspot/jtreg/gc/epsilon Running test 'jtreg:test/hotspot/jtreg/gc/epsilon' Passed: gc/epsilon/TestAlwaysPretouch.java Passed: gc/epsilon/TestAlignment.java Passed: gc/epsilon/TestElasticTLAB.java Passed: gc/epsilon/TestEpsilonEnabled.java Passed: gc/epsilon/TestHelloWorld.java Passed: gc/epsilon/TestLogTrace.java Passed: gc/epsilon/TestDieDefault.java Passed: gc/epsilon/TestDieWithOnError.java Passed: gc/epsilon/TestMemoryPools.java Passed: gc/epsilon/TestMaxTLAB.java Passed: gc/epsilon/TestPrintHeapSteps.java Passed: gc/epsilon/TestArraycopyCheckcast.java Passed: gc/epsilon/TestClasses.java Passed: gc/epsilon/TestUpdateCountersSteps.java Passed: gc/epsilon/TestDieWithHeapDump.java Passed: gc/epsilon/TestByteArrays.java Passed: gc/epsilon/TestManyThreads.java Passed: gc/epsilon/TestRefArrays.java Passed: gc/epsilon/TestObjects.java Passed: gc/epsilon/TestElasticTLABDecay.java Passed: gc/epsilon/TestSlidingGC.java Test results: passed: 21 TEST SUCCESS 

? fastdebug . ? - .



7.


- spring-petclinic , Apache Bench GC! , , GC .


: -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC :


:


 Heap: 20480M reserved, 20480M (100.00%) committed, 19497M (95.20%) used GC(2) Step 0: Prologue 2.085ms GC(2) Step 1: Mark 51.005ms GC(2) Step 2: Calculate new locations 71.207ms GC(2) Step 3: Adjust pointers 49.671ms GC(2) Step 4: Move objects 22.839ms GC(2) Step 5: Epilogue 1.008ms GC(2) GC Stats: 70561 (8.63%) reachable from roots, 746676 (91.37%) reachable from heap, 91055 (11.14%) moved, 2237 (0.27%) markwords preserved GC(2) Heap: 20480M reserved, 20480M (100.00%) committed, 37056K (0.18%) used GC(2) Lisp2-style Mark-Compact (Allocation Failure) 20479M->36M(20480M) 197.940ms 

200 ? GC! , . , , : ( — , ). - ( ).


, GC . , -Xlog:gc -XX:+UseSerialGC — , , :


 GC(46) Pause Young (Allocation Failure) 575M->39M(1943M) 2.603ms GC(47) Pause Young (Allocation Failure) 575M->39M(1943M) 2.606ms GC(48) Pause Young (Allocation Failure) 575M->39M(1943M) 2.747ms GC(49) Pause Young (Allocation Failure) 575M->39M(1943M) 2.578ms 

, 2 . , , GC . -Xlog:gc -XX:+UseSerialGC , , :


 GC(3) Pause Full (Allocation Failure) 16385M->34M(18432M) 1969.694ms GC(4) Pause Full (Allocation Failure) 16385M->34M(18432M) 2261.405ms GC(5) Pause Full (Allocation Failure) 16385M->34M(18432M) 2327.577ms GC(6) Pause Full (Allocation Failure) 16385M->34M(18432M) 2328.976ms 

, . .



8. ?


. , GC OpenJDK — , , .


:


  1. . , // . . , , « » , , .

    GC, java.lang.ref.Reference.referent — Java-, , , - . FinalReference , .
    ReferenceProcessor / / .
  2. VM. VM, , , . . , , , - , .


  3. . — , GC, . , , , .

    mark-compact GC Full GC fallbacks Shenandoah ( OpenJDK 8) G1 ( OpenJDK 10, JEP 307: «Parallel Full GC for G1» ).

  4. . , «» , , , - . . , .


  5. . , , , . , «» — «» «» , .


  6. - GC Handbook .




9.


? GC — , , , GC.


, - GC . , , GC (, Serial GC Parallel GC), .


Minute de publicité. , 5-6 2019, JPoint — Java-. — OpenJDK, GraalVM, Kotlin . .

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


All Articles