GC générationnels traditionnels (jdk6) VS GC Garbage First (jdk7)

L’analyse empirique montre que dans une application la très grande majorité des objets créés sont détruits presque immédiatement. C’est d’autant plus vrai pour les applications web et/ou stateless où la plupart des objets sont créés pour traiter une requête et peuvent être donc détruits juste après ce traitement. De ce constat résulte l’idée de ne pas traiter de la même façon les objets fraîchement créés et ceux qui existent depuis plus longtemps. Les Garbage Collector qui utilisent des implémentations basées sur ce principe sont appelés GC générationnels. On peut fixer deux catégories d’objectifs lorsque l’on optimise le GC : réduire les pauses ou augmenter le débit. Ces objectifs sont en général orthogonaux (la réduction de la durée des pauses se fait au détriment du débit, et vice-versa), ils dépendent souvent du type d’applications : dans une application interactive, nous privilégierons les pauses, et au contraire, dans un batch, seul le débit compte.

La suite du billet est décomposée en deux parties :

  • La première décrit le fonctionnement des algorithmes générationnels traditionnels (Implémentation actuelle des JVM Sun)
  • La seconde détaille le nouvel algorithme que Sun essaye de pousser pour sa future JVM

GC Générationels

Avant de présenter la nouvelle implémentation que Sun essaye de mettre en avant pour le jdk 7, décrivons le cycle de vie des objets dans le tas (heap) tel qu’il est actuellement implémenté dans les JVM Sun. Le « heap » se compose de trois sections principales « Young », « Tenured » et « Perm ». Au cours de sa vie, un objet est amené à évoluer au sein de ces zones en fonction de son ancienneté, à chaque section correspond une génération d’objet.

generations.jpg

Les nouveaux objets sont stockés dans une première zone de la « young generation » appelée « l’eden » (1). Lorsque cette zone est remplie, le garbage collector analyse les objets présents dans cette zone et nettoie les objets devenus inutiles (ne possédant plus de références).

Les objets ayant résisté à ce passage sont placés dans une seconde partie appelée « survivor » (2). Il s’agit d’une zone intermédiaire contenant les objets les plus vieux de la « young generation ». L’eden ayant été vidée lors du passage du GC, de nouveaux objets peuvent faire leur apparition dans cette même zone. À ce moment deux zones contiennent alors des objets : l’eden et le survivor. Dès lors, lors des futurs passages du GC, ces deux zones seront analysées et les objets toujours en activité seront placés dans une 2e zone « survivor ». Les deux zones « survivors » sont utilisées alternativement par le GC pour stocker les objets utilisés.

Avant que ces zones ne deviennent également trop petites et lorsqu’un objet est considéré comme assez âgé, il est déplacé dans la deuxième génération : « tenured » (3). Il s’agit d’un emplacement permettant de stocker les objets les plus anciens. Lorsque cette zone est remplie, le garbage collector effectue un nettoyage en profondeur des différentes zones et générations : le full GC. Si après un full GC l’espace disponible n’est pas suffisant pour gérer la vie de l’application, une exception est levée : java.lang.OutOfMemoryException.

Quant à la troisième section : « la perm », celle-ci stocke les objets n’ayant pas besoin d’être nettoyés par le GC, comme les classes chargées par le class loader lors de l’exécution d’un programme. Ces objets resteront disponibles pendant toute la durée de vie de la JVM. De la même manière, cette zone n’étant jamais nettoyée, si celle-ci vient à saturer, un OutOfMemory est également lancé : « java.lang.OutOfMemoryError: PermGen space » . Son fonctionnement est donc un peu différent : il n’y a pas de promotion dans cette zone.

Les zones « virtual » ne sont pas utilisées par la machine virtuelle pour stocker des données. Elles correspondent à l’écart entre la taille réelle et la taille max. Leur réservation n’a pas encore été faite auprès système. Elles n’existent que si on a choisi une taille adaptative (Taille Heap Min (-Xms) < Taille Heap Max (-Xmx)).

gc.jpg

Pour résumer, les objets sont créés dans « l’eden » (1), puis déplacé dans le « survivor » (2) puis dans le « Tenured » (3).

Le GC effectue deux types de traitements :

  • Un nettoyage rapide lorsque « l’eden » est rempli, comme il s’agit d’un algorithme de copie, la destruction d’un objet ne coûte rien : zéro inscrution \! Traitement parallélisable.
  • Un nettoyage approfondi, plus gourmand en ressources, lorsque le « tenured est saturé ». Il faut libérer la mémoire explicitement, et éventuellement la compacter (donc déplacer les objets et mettre à jour les références en conséquence) Plusieurs algo disponibles : Mark&Sweep, Concurrent Mark&Sweep, Incremental/train

L’un des objectifs de l’optimisation du GC est donc de limiter la fréquence des FullGC en s’assurant que le maximum d’objets sera ramassé avant sa promotion dans la old génération. Pour une application web, cela signifie généralement que l’intervalle entre 2 fullGC est supérieur au temps de traitement max d’une requête. Rappelons en passant que la méthode System.gc() déclenche un FullGC et devrait la plupart du temps être proscrite (-XX:+DisableExplicitGC)

GC Garbage First

L’idée de l’algorithme G1 (Garbage-First) est d’utiliser les spécificités des environnements multiprocesseurs afin d’approcher des performances ‘temps réel’ tout en maintenant un débit élevé. Pour parvenir à ce but, le tas n’est plus divisé en générations, mais en un nombre beaucoup plus grand de petites régions. Désormais, la collecte d’une génération dans son ensemble n’est maintenant plus obligatoire, elle peut se limiter à un ensemble précis de régions.

g1.jpg

La sélection de ces régions est possible grâce à un marquage spécifique : snapshot-at-the-beginning (SATB). Ce marquage, concurrent et très rapide, fournit au GC une analyse périodique et exhaustive de l’utilisation des différents objets en mémoire. Toutes les références des différents objets sont suivies. Leur examen permet la détection des objets qui ne sont plus utilisés : ils sont alors considérés comme déchets.

Un ensemble de métrique est également maintenu afin de permettre d’évaluer l’occupation des différentes régions du tas : taille maximale de la région, ancienneté, pourcentage d’occupation et surtout, pourcentage de déchets. De plus, les collectes passées permettent au GC de connaître relativement précisément le coût de la collecte d’une région particulière. En croisant ces évaluations (probabilités) avec le recensement des références dans les régions du système, le GC peut effectuer une sélection arbitraire d’un ensemble de régions nettoyable durant une période prédéfinie. La configuration du GC permet à l’utilisateur de spécifier une fraction de temps maximale consacrée à la collecte (par exemple : sur 100 secondes d’exécution de l’application, le nettoyage des données ne peut pas dépasser 5 secondes (5%)), ce qui revient presque à définir les horaires des collectes par configuration. Les régions privilégiées sont celles qui possèdent les données les plus jeunes et le plus de déchets, car elles proposent généralement un meilleur rendement : voici donc d’où vient le nom de l’algorithme « Garbage First ». Cette sélection de régions peut être considérée comme une young génération dont le contenu est défini dynamiquement : Garbage First est donc un algorithme générationnel. Par ailleurs, afin d’éviter de dépasser ce temps ‘objectif’, et donc pour d’optimiser le débit, la collecte peut-être reportée si nécessaire (dans la mesure du possible), peut être qu’au prochain passage plus de déchets seront présents dans une des régions.

Illustrons ces propos par un exemple :

Supposons que le tas est découpé en 12 régions. Les zones vertes sur les schémas suivants représentent des régions vides.

Le marquage a été effectué, il en résulte le découpage suivant. Le pourcentage affiché dans chacune des cases ci-dessous correspond au pourcentage d’objets vivants dans leur région associée. Ainsi dans la 2e case, 3% des objets présents dans celle-ci sont toujours utilisés, par conséquent cette région possède 97% de déchets. Les régions qui ne comptent pas d’objets vivants sont entièrement libérées. La collecte de zone ne contenant que des déchets est très rapide, on peu le comparer à celui utilisé dans les ‘survivor’ dans les algorithmes générationnels traditionnels.

jdk7-g1-sample1.jpg

L’application continue de tourner autant de temps que nécessaire pour remplir une nouvelle région intégralement : les nouveaux objets sont référencés dans une même région (case orange). Si un cycle de collection n’est pas suffisant pour remplir cette région, le GC ne se lance pas.

jdk7-g1-sample2.jpg

Maintenant que cette nouvelle région est remplie, le GC sélectionne les régions les contenant le plus de déchets qu’il estime pouvoir traiter simultanément (GabageFirst !). Les données vivantes restant dans ces régions sont copiées et rassemblées dans une autre région. Comme il s’agit d’un algorithme de copie, très peu de temps est nécessaire à l’exécution de cette tâche. Une fois que la copie terminée, les zones sélectionnées ne contiennent plus d’objets vivants, elles sont alors collectées.

jdk7-g1-sample3.jpg

Au final, si on récapitule, les avantages avancés de ce nouvel algorithme sont les suivants :

  • Meilleure prédiction des temps de pauses destinées au GC.
  • Courte pause sans fragmentation.
  • Nettoyage acceptant la parallélisassions et la concurrence.
  • Meilleure utilisation du tas.

En plus d’améliorer le débit des collectes, comme le tas n’est pas divisé en générations, les problèmes de sizing des différentes générations ne sont plus d’actualité : fini les prises de tête avec le calcul des ratios young / tenured générations.

Informations complémentaires à propos de ce nouvel algorithme :

Billets sur le même thème :

16 commentaires

  • Excellent article, merci !

  • Article très intéressant. Bravo !
    Nicolas

  • Merci pour l’article
    Mais comment se fait le partage entre la Young Tenured et Perm pour un Xmx=Xms=2G ?

  • Bonjour Christophe,

    Le sizing des différentes générations peut s’effectuer grâce à des ratios. Ceux-ci sont configurables par l’intermédiaire d’options spécifiques.

    Par exemple, l’option -XX:NewRatio=3 permet de définir le ratio entre la jeune et l’ancienne génération de 3 pour 1 : soit pour un tas à 2go, un young à 512mo et une tenured à 1536mo. Dans la plupart des cas, la valeur par défaut pour cette option est « 8″. De ce fait, les objets sont (trop ?) rapidement promus dans la seconde génération.

    Sur le même principe, l’option -XX:SurvivorRatio=6 permet de définir le ratio entre l’eden et les survivors. Pour un tas de 2 go, cela nous fait 384mo pour l’eden, et 64mo pour chaque survivor (1/8 de la young génération).

    En espérant avoir été clair.

    Erwan

  • bonjour erwan,
    J’ai gardé l’option par défaut d’un serveur (soit 2)
    Je suis donc à 1/3 et 2/3
    Mon problème est que la mémoire tenured explose malgré le full GC et que les hrpof généré ne sont pas lisible (trop gros)
    que faire ??

  • Article très instructif.
    Ce genre d’article se fait très rare, surtout en français.
    Bravo et merci !

  • Salut,

    Petite remarque sémantique : « Ces objectifs sont en général orthogonaux (la réduction de la durée des pauses se fait au détriment du débit, et vice-versa) »

    Justement non, s’ils sont orthogonaux, c’est qu’ils sont libres/indépendants l’un par rapport à l’autre, cf l’analogie avec les produits scalaires.

    Tu devrais plutôt dire « opposé », « antinomiques ».

    A part ça, intéressant. J’aimerais savoir ce qu’il faut face à celui d’OCaml.

  • Pour répondre à :
    > Mais comment se fait le partage entre la Young Tenured et Perm pour un Xmx=Xms=2G ?

    En fait, c’est simple: la taille de la Perm n’est pas comprise dans les zones définies par Xmx et Xms. C’est une zone « en plus », et elle est définie par un « -XX:MaxPermSize=256m ».

  • Bonjour Erwan,

    Félicitations pour cet article, il est vraiment très clair !

    Cédric

  • Merci pour cet article!!
    Par contre, je ne comprends pas comment le GC utilise « les spécificités des environnements multiprocesseurs ». Ca voudrait dire que le GC tourne en parallèle de l’application? Pourquoi lui spécifier une fraction de temps maximale dans ce cas?

  • Bonjour,

    J’ai une application qui tourne sur tomcat( sur eclipse)
    J’obtiens le message d’erreur suivant à chaque rechargement du server.
    Pouvez vous m’aider à corriger cet erreur.

    Merci d’avance

    [CODE]
    10:19:57,980 INFO SessionFactoryImpl:379 – Checking 0 named queries
    Exception in thread « Timer-0″ java.lang.NullPointerException
    at com.mchange.v2.log.log4j.Log4jMLog$Log4jMLogger.isLoggable(Log4jMLog.java:257)
    at com.mchange.v2.resourcepool.BasicResourcePool$CullTask.run(BasicResourcePool.java:1934)
    at java.util.TimerThread.mainLoop(Timer.java:512)
    at java.util.TimerThread.run(Timer.java:462)
    [/CODE]

  • Bonjour,
    Merci pour cet article.
    J’ai quelques petites remarques que j’aimerais partager avec vous sur les GC générationnels :

    - « On peut fixer deux catégories d’objectifs lorsque l’on optimise le GC : réduire les pauses ou augmenter le débit »
    >> Il n’y a pas que ça, voici les métriques qu’il faut prendre en compte pour le tuning de la jvm : le temps de pause, le throughput, le GC overhead, la fréquence des collections, le footprint

    - « De la même manière, cette zone n’étant jamais nettoyée » (En parlant de la perm)
    >> Attention, la perm gen est nettoyé aussi. Rien n’échappe au full GC.

    - « « tenured » (3). Il s’agit d’un emplacement permettant de stocker les objets les plus anciens »
    >> Les plus gros aussi, car il y a des objets qui seront placés directement dans la old génération sans faire le passage eden-> survivor

    - « (Taille Heap Min (-Xms) > Pour info, ce paramétrage fortement déconseillée Xms doit être égale à Xmx.

    - « Plusieurs algo disponibles : Mark&Sweep, Concurrent Mark&Sweep, Incremental/train »
    >> Il n’est plus disponible depuis la 1.4.2

    >> Voici en résumé les algorithme de la young generation :
    - copying collector (Par défaut)
    - parallel copying collector (-XX:+UseParNewGC)
    - parallel scavenge collector (-XX:UseParallelGC)

    Et ceux de la old generation :
    - mark-sweep collector (Par défaut)
    - concurrent collector ou CMS (-XX:+UseConcMarkSweepGC)
    - incremental collector (-Xincgc) (Il n’est plus disponible depuis la 1.4.2)

    Et voici un lien pour lire sur le Garbage First Collector :
    http://download.java.net/jdk7/docs/technotes/guides/vm/G1.html

  • Une très bonne intro aux GC (en anglais): http://www.infoq.com/presentations/Understanding-Java-Garbage-Collection

  • Merci pour cet excellant article, bien detaillé d’une manière très simple

Laisser un commentaire