Pourquoi java attendre si longtemps pour exécuter le garbage collector?

Je construis une application Web Java, en utilisant le jeu ! Cadre . Je l'héberge sur playapps.net . j'ai été perplexe pendant un certain temps sur les graphiques fournis de la consommation de mémoire. Voici un exemple:

La Mémoire Du Tas

Le graphique provient d'une période d'activité constante mais nominale. Je n'ai rien fait pour déclencher la chute en mémoire, donc je suppose que cela s'est produit parce que le garbage collector a couru car il a presque atteint sa mémoire admissible consommation.

Mes questions:

  • est-il juste pour moi de supposer que mon application ne pas {[21] } ont une fuite de mémoire, car il semble que toute la mémoire est correctement récupérée par le garbage collector quand il fonctionne?
  • (à partir du titre) pourquoi java attend-il la dernière seconde possible pour exécuter le garbage collector? Je vois une dégradation significative des performances à mesure que la consommation de mémoire atteint le quart supérieur de la graphique.
  • Si mes affirmations ci-dessus sont correctes, alors comment puis-je résoudre ce problème? Les autres messages que j'ai lus semblent donc opposés aux appels à System.gc(), allant de neutre ("c'est seulement une requête pour exécuter GC, donc la JVM peut simplement vous ignorer") à carrément opposé ("le code qui repose sur System.gc() est fondamentalement cassé"). Ou suis-je hors de la base ici, et je devrais chercher des défauts dans mon propre code qui causent ce comportement et une perte de performance intermittente?

Mise à JOUR
J'ai ouvert une discussion sur PlayApps.net pointant vers cette question et mentionnant certains des points ici; en particulier le commentaire de @ Affe concernant les paramètres d'un GC complet étant défini de manière très conservatrice, et le commentaire de @G_H sur les paramètres pour la taille initiale et maximale du tas.

Voici un lien vers la discussion , bien que vous ayez malheureusement besoin d'un compte playapps pour l'afficher.

Je vais signaler les commentaires ici quand je l'obtiens; merci beaucoup à tous pour vos réponses, j'ai déjà appris beaucoup d'eux!

Résolution
Le support de Playapps, qui est toujours génial, n'a pas eu beaucoup de suggestions pour moi, leur seule pensée étant que si j'utilisais largement le cache, cela peut garder les objets en vie plus longtemps que nécessaire, mais ce n'est pas le cas. J'ai encore appris une tonne (woo hoo!), et j'ai donné à @Ryan Amos le chèque vert alors que j'ai pris sa suggestion d'appeler System.gc() chaque demi-journée, ce qui est pour l'instant fonctionne très bien.

48
demandé sur ROMANIA_engineer 2011-08-19 01:59:52

6 réponses

Java n'exécutera pas le nettoyeur de déchets tant qu'il ne le doit pas, car le nettoyeur de déchets ralentit un peu les choses et ne devrait pas être exécuté aussi fréquemment. Je pense que vous seriez OK pour planifier un nettoyage plus fréquemment, comme toutes les 3 heures. Si une application ne consomme jamais de mémoire complète, il ne devrait y avoir aucune raison d'exécuter le nettoyeur de déchets, c'est pourquoi Java ne l'exécute que lorsque la mémoire est très élevée.

Donc, fondamentalement, ne vous inquiétez pas de ce que les autres disent: faites ce qui fonctionne le mieux. Si vous trouvez des améliorations de performance en exécutant le nettoyeur d'ordures à 66% de mémoire, faites-le.

19
répondu Ryan Amos 2011-08-18 22:03:48

Toute réponse détaillée dépendra du garbage collector que vous utilisez, mais il y a des choses qui sont fondamentalement les mêmes dans tous les GCs (modern, sun/oracle).

Chaque fois que vous voyez l'utilisation dans le graphique descendre, c'est une récupération de place. La seule façon dont heap est libéré est par la collecte des ordures. Le fait est qu'il existe deux types de collectes de déchets, mineures et complètes. Le tas est divisé en deux zones de base."Les jeunes et les titulaires. (Il ya beaucoup plus de sous-groupes dans la réalité.) Tout ce qui prend de la place chez Young et est encore utilisé lorsque le GC mineur arrive pour libérer de la mémoire, va être "promu" en permanent. Une fois que quelque chose fait le saut dans tenured, il reste indéfiniment jusqu'à ce que le tas n'ait pas d'espace libre et qu'une collecte complète des ordures soit nécessaire.

Donc, une interprétation de ce graphique est que votre jeune génération est assez petite (par défaut, elle peut être un % assez petit du tas total sur certaines JVM) et vous gardez les objets "vivants" pendant des temps relativement très longs. (peut-être que vous tenez des références à eux dans la session web?) Donc, vos objets "survivent" aux collectes de déchets jusqu'à ce qu'ils soient promus dans l'espace permanent, où ils restent indéfiniment jusqu'à ce que la JVM soit bien et bien vraiment hors de mémoire.

Encore une fois, c'est juste une situation courante qui correspond aux données que vous avez. Aurait besoin de détails complets sur la configuration de la JVM et les journaux GC pour vraiment dire à coup sûr ce qui se passe.

22
répondu Affe 2011-08-18 22:12:31

Je remarque que le graphique n'est pas en pente strictement vers le haut jusqu'à la chute, mais a de plus petites variations locales. Bien que je ne sois pas certain, Je ne pense pas que l'utilisation de la mémoire montrerait ces petites gouttes s'il n'y avait pas de collecte de déchets.

Il existe des collections mineures et majeures en Java. Les collections mineures sont fréquentes, alors que les collections majeures sont plus rares et diminuent davantage le rendement. Les collections mineures ont probablement tendance à balayer des choses comme des instances d'objets de courte durée créées dans les méthodes. Une collection majeure supprimera beaucoup plus, ce qui est probablement arrivé à la fin de votre graphique.

Maintenant, certaines réponses qui ont été postées pendant que je tape ceci donnent de bonnes explications concernant les différences entre les garbage collectors, les générations d'objets et plus encore. Mais cela n'explique toujours pas pourquoi il faudrait si absurdement long (près de 24 heures) avant un nettoyage sérieux est fait.

Deux choses d'intérêt qui peuvent être définies pour une JVM au démarrage sont le maximum taille de tas autorisée, et la taille de tas initiale. Le maximum est une limite difficile, une fois que vous atteignez cela, la récupération de place supplémentaire ne réduit pas l'utilisation de la mémoire et si vous avez besoin d'allouer un nouvel espace pour les objets ou d'autres données, vous obtiendrez une OutOfMemoryError. Cependant, en interne, il y a aussi une limite douce: la taille du tas actuel. Une JVM n'engloutit pas immédiatement la quantité maximale de mémoire. Au lieu de cela, il commence à votre taille de tas initiale, puis augmente le tas quand c'est nécessaire. Imaginez un peu que la RAM de votre JVM, qui peut augmenter dynamiquement.

Si l'utilisation réelle de la mémoire de votre application commence à atteindre la taille de tas actuelle, une récupération de place sera généralement initiée. Cela peut réduire l'utilisation de la mémoire, donc une augmentation de la taille du tas n'est pas nécessaire. Mais il est également possible que l'application ait actuellement besoin de toute cette mémoire et dépasse la taille du tas. Dans ce cas, il est augmenté à condition qu'il n'ait pas déjà atteint la limite maximale fixée.

Maintenant, ce qui pourrait être votre cas, c'est que la taille initiale du tas est définie sur la même valeur que le maximum. Supposons que ce soit le cas, alors la JVM saisira immédiatement toute cette mémoire. Il faudra beaucoup de temps avant que l'application ait accumulé suffisamment de déchets pour atteindre la taille du tas dans l'utilisation de la mémoire. Mais à ce moment, vous verrez une grande collection. En commençant par un tas assez petit et en lui permettant de croître, l'utilisation de la mémoire reste limitée à ce qui est nécessaire.

Cela suppose que votre graphique affiche l'utilisation du tas et la taille du tas non allouée. Si ce n'est pas le cas et que vous voyez réellement le tas lui-même grandir comme ça, quelque chose d'autre se passe. J'admets que je ne suis pas assez averti en ce qui concerne les internes de la collecte des ordures et sa planification pour être absolument certain de ce qui se passe ici, la plupart de cela provient de l'observation des applications qui fuient dans les profileurs. Donc, si j'ai fourni des informations défectueuses, je vais prendre cette réponse vers le bas.

12
répondu G_H 2016-08-31 11:02:50

Comme vous l'avez peut-être remarqué, cela ne vous affecte pas. La garbage collection n'intervient que si la JVM estime qu'il est nécessaire qu'elle s'exécute et que cela se produit par souci d'optimisation, il n'est pas utile de faire beaucoup de petites collections si vous pouvez faire une seule collection complète et faire un nettoyage complet.

La JVM actuelle contient des algorithmes vraiment intéressants et l'ID de garbage collection lui-même divisé en 3 régions différentes, vous pouvez trouver beaucoup plus à ce sujet ici , voici un exemple:

Trois types d'algorithmes de collecte

La JVM HotSpot fournit trois algorithmes GC, chacun réglé pour un type spécifique de collection au sein d'une génération spécifique. La collection copy (également connue sous le nom de scavenge) nettoie rapidement les objets de courte durée dans le tas de nouvelle génération. L'algorithme mark-compact utilise une technique plus lente et plus robuste pour collecter des objets de plus longue durée dans le tas de l'ancienne génération. L'algorithme incrémental tente d'améliorer l'ancien collecte de génération en effectuant une GC robuste tout en minimisant les pauses.

Copier/récupérer la collection

En utilisant l'algorithme de copie, la JVM récupère la plupart des objets de l'espace objet de nouvelle génération (également connu sous le nom d'eden) simplement en faisant de petits charognards - un terme Java pour collecter et supprimer les déchets. Les objets à durée de vie plus longue sont finalement copiés, ou maintenus, dans l'ancien espace d'objets.

Marque-collection compacte

Comme plus d'objets deviennent titulaires, l'ancien l'espace de l'objet commence à atteindre l'occupation maximale. L'algorithme mark-compact, utilisé pour collecter des objets dans l'ancien espace objet, a des exigences différentes de celles de l'algorithme de collection de copies utilisé dans le nouvel espace objet.

L'algorithme mark-compact scanne d'abord tous les objets, marquant ainsi tous les objets accessibles. Il compacte ensuite toutes les lacunes restantes des objets morts. L'algorithme mark-compact occupe plus de temps que l'algorithme de collection de copies; cependant, il nécessite moins de mémoire et élimine la fragmentation de la mémoire.

Collection incrémentale (train)

Les algorithmes copy/scavenge de nouvelle génération et mark-compact de l'ancienne génération ne peuvent pas éliminer toutes les pauses JVM. Ces pauses sont proportionnelles au nombre d'objets vivants. Pour répondre au besoin de GC sans pause, la JVM HotSpot offre également une collecte incrémentale, ou train.

La collection incrémentielle décompose les anciennes pauses de la collection d'objets en de nombreuses pauses minuscules, même avec de grandes zones d'objets. Plutôt juste une nouvelle et une ancienne génération, cet algorithme a une génération intermédiaire comprenant de nombreux petits espaces. Il y a des frais généraux associés à la collecte incrémentielle; vous pourriez voir autant qu'une dégradation de la vitesse de 10%.

Les paramètres-Xincgc et-Xnoincgc contrôlent la façon dont vous utilisez la collection incrémentielle. La prochaine version de HotSpot JVM, version 1.4, tentera GC continu, sans pause qui sera probablement une variation de l'algorithme incrémental. Je ne parlerai pas d'incrémental collection car elle va bientôt changer.

Ce garbage collector générationnel est l'une des solutions les plus efficaces que nous avons pour le problème de nos jours.

2
répondu Maurício Linhares 2011-08-18 22:09:10

J'ai eu une application qui a produit un graphique comme ça et a agi comme vous le décrivez. J'utilisais le collecteur CMS (- XX: + UseConcMarkSweepGC). Voici ce qui se passait dans mon cas.

Je n'avais pas assez de mémoire configurée pour l'application, donc au fil du temps, je rencontrais des problèmes de fragmentation dans le tas. Cela a causé GCs avec une fréquence de plus en plus grande, mais il n'a pas réellement jeté un OOME ou échoué DE CMS au collecteur série (ce qu'il est censé faire dans ce cas) parce que les statistiques qu'il conserve ne comptent que le temps de pause de l'application (GC bloque le monde), le temps simultané de l'application (GC s'exécute avec des threads d'application) est ignoré pour ces calculs. J'ai réglé certains paramètres, principalement lui a donné une charge de merde entière plus de tas (avec un très grand nouvel espace), set-XX:CMSFullGCsBeforeCompaction=1, et le problème a cessé de se produire.

2
répondu Kevin Lafayette 2012-01-30 21:52:45

Vous avez probablement des fuites de mémoire qui sont effacées toutes les 24 heures.

0
répondu irreputable 2011-08-18 23:49:38