web-dev-qa-db-fra.com

Traquer un problème de fuite de mémoire / de récupération de place dans Java

C'est un problème que j'essaie de retrouver depuis quelques mois maintenant. J'ai une application Java en cours d'exécution qui traite les flux xml et stocke le résultat dans une base de données. Il y a eu des problèmes intermittents de ressources qui sont très difficiles à détecter.

Contexte: Sur la boîte de production (où le problème est le plus visible), je n'ai pas un accès particulièrement bon à la boîte et je n'ai pas pu faire fonctionner Jprofiler. Cette boîte est une machine 64 bits quad-core, 8 Go exécutant centos 5.2, Tomcat6 et Java 1.6.0.11. Cela commence par ces options Java

Java_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

La pile technologique est la suivante:

  • Centos 64 bits 5.2
  • Java 6u11
  • Tomcat 6
  • Spring/WebMVC 2.5
  • Hibernate 3
  • Quartz 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (et bien sûr un hôte d'autres dépendances, notamment les bibliothèques jakarta-commons)

Le plus proche que je peux obtenir pour reproduire le problème est une machine 32 bits avec moins de mémoire. Que j'ai le contrôle. Je l'ai sondé à mort avec JProfiler et corrigé de nombreux problèmes de performances (problèmes de synchronisation, précompilation/mise en cache des requêtes xpath, réduction du pool de threads et suppression de la préchargement hibernate inutile et du "réchauffement du cache" trop zélé pendant le traitement).

Dans chaque cas, le profileur a montré que celles-ci prenaient d'énormes quantités de ressources pour une raison ou une autre, et qu'il ne s'agissait plus de porcs de ressources primaires une fois les changements entrés.

Le problème: La JVM semble ignorer complètement les paramètres d'utilisation de la mémoire, remplit toute la mémoire et ne répond plus. C'est un problème pour le client face à la fin, qui attend un sondage régulier (base de 5 minutes et nouvelle tentative de 1 minute), ainsi que pour nos équipes opérationnelles, qui sont constamment avisées qu'une boîte ne répond plus et doivent la redémarrer. Il n'y a rien d'autre de significatif sur cette box.

Le problème apparaît comme un garbage collection. Nous utilisons le collecteur ConcurrentMarkSweep (comme indiqué ci-dessus) car le collecteur STW d'origine provoquait des délais d'expiration JDBC et devenait de plus en plus lent. Les journaux montrent qu'à mesure que l'utilisation de la mémoire augmente, cela commence à générer des échecs de cms et revient au collecteur d'origine qui semble ne pas collecter correctement.

Cependant, en cours d'exécution avec jprofiler, le bouton "Exécuter GC" semble nettoyer la mémoire plutôt que de montrer une empreinte croissante, mais comme je ne peux pas connecter jprofiler directement à la boîte de production, et la résolution des points d'accès éprouvés ne semble pas fonctionner, je le suis gauche avec le vaudou de réglage aveugle Garbage Collection.

Ce que j'ai essayé:

  • Profilage et correction des points chauds.
  • Utilisation des récupérateurs de place STW, Parallel et CMS.
  • Fonctionnement avec des tailles de tas min/max à 1/2,2/4,4/5,6/6 incréments.
  • Fonctionnant avec un espace permgen par incréments de 256 Mo jusqu'à 1 Go.
  • Beaucoup de combinaisons de ce qui précède.
  • J'ai également consulté la JVM [référence de réglage] (http://Java.Sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html), mais je ne trouve vraiment rien expliquant ce comportement ou des exemples de réglage de _which_ paramètres à utiliser dans une situation comme celle-ci.
  • J'ai également (sans succès) essayé jprofiler en mode hors ligne, en me connectant avec jconsole, visualvm, mais je n'arrive pas à trouver quoi que ce soit qui puisse interférer avec mes données de journal gc.

Malheureusement, le problème apparaît également sporadiquement, il semble être imprévisible, il peut fonctionner pendant des jours ou même une semaine sans aucun problème, ou il peut échouer 40 fois par jour, et la seule chose que je peux sembler attraper de manière cohérente est cette collecte des ordures agit.

Quelqu'un peut-il donner des conseils sur:
a) Pourquoi une machine virtuelle Java utilise 8 concerts physiques et 2 Go d'espace de swap lorsqu'elle est configurée pour un maximum de moins de 6.
b) Une référence à l'optimisation GC qui explique ou donne des exemples raisonnables de quand et avec quel type de paramètre utiliser les collections avancées.
c) Une référence aux fuites de mémoire les plus courantes Java (je comprends les références non réclamées, mais je veux dire au niveau de la bibliothèque/du framework, ou quelque chose de plus inhérent aux structures de données, comme les hashmaps).

Merci pour toutes les informations que vous pouvez fournir.

MODIFIER
Emil H:
1) Oui, mon cluster de développement est un miroir des données de production, jusqu'au serveur multimédia. La principale différence est le 32/64bit et la quantité de RAM disponible, que je ne peux pas reproduire très facilement, mais le code et les requêtes et paramètres sont identiques.

2) Il existe un certain code hérité qui repose sur JaxB, mais en réorganisant les travaux pour essayer d'éviter les conflits de planification, j'ai généralement cette exécution éliminée car elle s'exécute une fois par jour. L'analyseur principal utilise des requêtes XPath qui appellent le package Java.xml.xpath. C'était la source de quelques hotspots, pour l'un les requêtes n'étaient pas pré-compilées, et deux les références à elles étaient dans des chaînes codées en dur. J'ai créé un cache threadsafe (hashmap) et pris en compte les références aux requêtes xpath pour qu'elles soient des chaînes statiques finales, ce qui a considérablement réduit la consommation de ressources. L'interrogation est toujours une grande partie du traitement, mais cela devrait être parce que c'est la responsabilité principale de l'application.

3) Une note supplémentaire, l'autre consommateur principal est les opérations d'image de JAI (retraitement des images d'un flux). Je ne connais pas les bibliothèques graphiques de Java, mais d'après ce que j'ai trouvé, elles ne sont pas particulièrement fuyantes.

(merci pour les réponses jusqu'à présent, les amis!)

MISE À JOUR:
J'ai pu me connecter à l'instance de production avec VisualVM, mais il avait désactivé l'option de visualisation/run-GC du GC (même si je pouvais le visualiser localement). La chose intéressante: l'allocation de tas de VM obéit à Java_OPTS, et le tas alloué réel est assis confortablement à 1-1,5 concerts, et ne semble pas fuir, mais la surveillance au niveau de la boîte montre toujours un modèle de fuite , mais cela ne se reflète pas dans la surveillance VM. Il n'y a rien d'autre sur cette boîte, donc je suis perplexe.

79
liam

Eh bien, j'ai finalement trouvé le problème à l'origine de cela, et je poste une réponse détaillée au cas où quelqu'un d'autre aurait ces problèmes.

J'ai essayé jmap pendant que le processus progressait, mais cela provoquait généralement le blocage de jvm, et je devais l'exécuter avec --force. Cela a entraîné des vidages de tas qui semblaient manquer beaucoup de données, ou au moins manquer les références entre eux. Pour l'analyse, j'ai essayé le jhat, qui présente beaucoup de données mais pas beaucoup sur la façon de l'interpréter. Deuxièmement, j'ai essayé l'outil d'analyse de mémoire basé sur Eclipse ( http://www.Eclipse.org/mat/ ), qui a montré que le tas était principalement des classes liées à Tomcat.

Le problème était que jmap ne signalait pas l'état réel de l'application et n'attrapait les classes qu'à l'arrêt, qui étaient principalement des classes Tomcat.

J'ai essayé plusieurs fois de plus et j'ai remarqué qu'il y avait un nombre très élevé d'objets modèles (en fait 2 à 3 fois plus que ce qui était marqué public dans la base de données).

À l'aide de cela, j'ai analysé les journaux de requêtes lentes et quelques problèmes de performances non liés. J'ai essayé le chargement extra-paresseux ( http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html ), ainsi que le remplacement de quelques opérations d'hibernation par les requêtes jdbc directes (principalement lorsqu'il s'agissait de charger et d'opérer sur de grandes collections - les remplacements jdbc fonctionnaient directement sur les tables de jointure), et ont remplacé certaines autres requêtes inefficaces que mysql journalisait.

Ces étapes ont amélioré certaines des performances frontales, mais n'ont toujours pas résolu le problème de la fuite, l'application était toujours instable et agissait de manière imprévisible.

Enfin, j'ai trouvé l'option: -XX: + HeapDumpOnOutOfMemoryError. Cela a finalement produit un très gros fichier hprof (~ 6,5 Go) qui montrait avec précision l'état de l'application. Ironiquement, le fichier était si volumineux que jhat ne pouvait pas l'anaylze, même sur une boîte avec 16 Go de RAM. Heureusement, MAT a été en mesure de produire de beaux graphiques et a montré de meilleures données.

Cette fois, ce qui restait était un seul fil de quartz occupait 4,5 Go des 6 Go de tas, et la majorité était un StatefulPersistenceContext hibernate ( https://www.hibernate.org/hib_docs/v3/api /org/hibernate/engine/StatefulPersistenceContext.html ). Cette classe est utilisée par hibernate en interne comme cache principal (j'avais désactivé le deuxième niveau et les caches de requête soutenus par EHCache).

Cette classe est utilisée pour activer la plupart des fonctionnalités de la mise en veille prolongée, elle ne peut donc pas être directement désactivée (vous pouvez la contourner directement, mais Spring ne prend pas en charge la session sans état), et je serais très surpris si cela avait un tel fuite de mémoire importante dans un produit mature. Alors pourquoi fuyait-il maintenant?

Eh bien, c'était une combinaison de choses: le pool de fils de quartz instancie avec certaines choses étant threadLocal, le printemps injectait une usine de session dans, qui créait une session au début du cycle de vie des fils de quartz, qui était ensuite réutilisée pour exécuter le divers travaux de quartz qui ont utilisé la session d'hibernation. Hibernate était alors en cache dans la session, ce qui est son comportement attendu.

Le problème est alors que le pool de threads n'a jamais libéré la session, donc hibernate restait résident et maintenait le cache pour le cycle de vie de la session. Comme cela utilisait le support du modèle de mise en veille prolongée de springs, il n'y avait pas d'utilisation explicite des sessions (nous utilisons une hiérarchie dao -> manager -> driver -> quartz-job, le dao est injecté avec des configurations d'hibernation à travers le printemps, donc les opérations sont fait directement sur les modèles).

Ainsi, la session n'était jamais fermée, hibernate conservait des références aux objets de cache, donc ils n'étaient jamais récupérés, donc chaque fois qu'un nouveau travail s'exécutait, il ne faisait que remplir le cache local sur le thread, donc il n'y avait même pas tout partage entre les différents métiers. De plus, comme il s'agit d'un travail intensif en écriture (très peu de lecture), le cache était principalement gaspillé, donc les objets continuaient à être créés.

La solution: créez une méthode dao qui appelle explicitement session.flush () et session.clear (), et appelez cette méthode au début de chaque travail.

L'application fonctionne depuis quelques jours sans aucun problème de surveillance, erreur de mémoire ou redémarrage.

Merci pour l'aide de tout le monde à ce sujet, c'était un bug assez difficile à localiser, car tout faisait exactement ce qu'il était censé faire, mais au final, une méthode en 3 lignes a réussi à résoudre tous les problèmes.

90
liam

Il semble que la mémoire autre que le tas fuit, vous mentionnez que le tas reste stable. Un candidat classique est permgen (génération permanente) qui se compose de 2 choses: les objets de classe chargés et les chaînes internes. Étant donné que vous déclarez vous être connecté à VisualVM, vous devriez pouvoir voir la quantité de classes chargées, s'il y a une augmentation continue des classes chargées (important, visualvm affiche également la quantité totale de classes jamais chargées, ce n'est pas grave si cela augmente, mais la quantité de classes chargées devrait se stabiliser après un certain temps).

S'il s'avère qu'il s'agit d'une fuite de permgen, le débogage devient plus délicat car l'outillage pour l'analyse de permgen est plutôt manquant par rapport au tas. Votre meilleur pari est de démarrer un petit script sur le serveur qui invoque à plusieurs reprises (toutes les heures?):

jmap -permstat <pid> > somefile<timestamp>.txt

jmap avec ce paramètre générera une vue d'ensemble des classes chargées ainsi qu'une estimation de leur taille en octets, ce rapport peut vous aider à identifier si certaines classes ne sont pas déchargées. (Remarque: avec je veux dire l'ID du processus et devrait être un horodatage généré pour distinguer les fichiers)

Une fois que vous avez identifié certaines classes comme étant chargées et non déchargées, vous pouvez déterminer mentalement où elles pourraient être générées, sinon vous pouvez utiliser jhat pour analyser les vidages générés avec jmap -dump. Je garderai cela pour une future mise à jour si vous avez besoin des informations.

4
Boris Terzic

Pouvez-vous exécuter la boîte de production avec JMX activé?

-Dcom.Sun.management.jmxremote
-Dcom.Sun.management.jmxremote.port=<port>
...

Surveillance et gestion à l'aide de JMX

Et puis attachez avec JConsole, VisualVM ?

Est-il correct de faire un vidage de tas avec jmap ?

Si oui, vous pouvez alors analyser le vidage de tas pour les fuites avec JProfiler (vous l'avez déjà), jhat , VisualVM, Eclipse MAT . Comparez également les vidages de tas qui pourraient aider à trouver des fuites/modèles.

Et comme vous l'avez mentionné jakarta-commons. Il y a un problème lors de l'utilisation de jakarta-commons-logging lié à la conservation du chargeur de classe. Pour une bonne lecture sur ce chèque

n jour dans la vie d'un chasseur de fuites de mémoire ( release(Classloader) )

4
jitter

Je rechercherais ByteBuffer directement alloué.

Du javadoc.

Un tampon d'octets direct peut être créé en appelant la méthode d'usine allocateDirect de cette classe. Les tampons renvoyés par cette méthode ont généralement des coûts d'allocation et de désallocation quelque peu plus élevés que les tampons non directs. Le contenu des tampons directs peut résider en dehors du segment de mémoire normalement récupéré, et donc leur impact sur l'empreinte mémoire d'une application peut ne pas être évident. Il est donc recommandé d'allouer des tampons directs principalement aux grands tampons à longue durée de vie qui sont soumis aux opérations d'E/S natives du système sous-jacent. En général, il est préférable d'allouer des tampons directs uniquement lorsqu'ils produisent un gain mesurable dans les performances du programme.

Peut-être que le code Tomcat utilise cette fonction pour les E/S; configurer Tomcat pour utiliser un connecteur différent.

A défaut, vous pourriez avoir un thread qui exécute périodiquement System.gc (). "-XX: + ExplicitGCInvokesConcurrent" pourrait être une option intéressante à essayer.

2
Sean McCauliff

J'ai eu le même problème, avec quelques différences ..

Ma technologie est la suivante:

grails 2.2.4

Tomcat7

plugin quartz 1.0

J'utilise deux sources de données sur mon application. C'est un élément déterminant des causes de bugs.

Une autre chose à considérer est que le quartz-plugin, injecte une session de mise en veille prolongée dans des fils de quartz, comme le dit @liam, et des fils de quartz toujours en vie, jusqu'à ce que je termine l'application.

Mon problème était un bug sur l'ORM Grails combiné avec la façon dont le plugin gère la session et mes deux sources de données.

Le plugin Quartz avait un auditeur pour lancer et détruire les sessions de mise en veille prolongée

public class SessionBinderJobListener extends JobListenerSupport {

    public static final String NAME = "sessionBinderListener";

    private PersistenceContextInterceptor persistenceInterceptor;

    public String getName() {
        return NAME;
    }

    public PersistenceContextInterceptor getPersistenceInterceptor() {
        return persistenceInterceptor;
    }

    public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) {
        this.persistenceInterceptor = persistenceInterceptor;
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.init();
        }
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.flush();
            persistenceInterceptor.destroy();
        }
    }
}

Dans mon cas, persistenceInterceptor instances AggregatePersistenceContextInterceptor, et il y avait une liste de HibernatePersistenceContextInterceptor. Un pour chaque source de données.

Chaque opération fait avec AggregatePersistenceContextInterceptor son passé à HibernatePersistence, sans aucune modification ou traitement.

Lorsque nous appelons init() sur HibernatePersistenceContextInterceptor il incrémente la variable statique ci-dessous

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

Je ne connais pas le but de ce compte statique. Je sais juste qu'il est incrémenté deux fois, une par source de données, en raison de l'implémentation de AggregatePersistence.

Jusqu'ici, je viens d'expliquer le cénario.

Le problème vient maintenant ...

Lorsque mon travail de quartz est terminé, le plugin appelle l'auditeur pour vider et détruire les sessions d'hibernation, comme vous pouvez le voir dans le code source de SessionBinderJobListener.

Le vidage se produit parfaitement, mais pas la destruction, car HibernatePersistence, effectuez une validation avant de fermer la session d'hibernation ... Il examine nestingCount pour voir si la valeur est plus gratnante que 1. Si la réponse est oui, il n'a pas fermé la session.

Simplifier ce qui a été fait par Hibernate:

if(--nestingCount.getValue() > 0)
    do nothing;
else
    close the session;

C'est la base de ma fuite de mémoire. Des threads de quartz sont toujours vivants avec tous les objets utilisés en session, car Grails ORM ne ferme pas la session, à cause d'un bug causé parce que j'ai deux sources de données.

Pour résoudre ce problème, je personnalise l'écouteur, pour appeler clear avant destroy, et appeler destroy deux fois (un pour chaque source de données). S'assurer que ma session était claire et détruite, et si la destruction échoue, il était clair au moins.

1
jpozorio

Du JAXB? Je trouve que JAXB est un remplisseur d'espace permanent.

De plus, je trouve que visualgc , maintenant livré avec JDK 6, est un excellent moyen de voir ce qui se passe en mémoire. Il montre magnifiquement les espaces eden, générationnel et permanent et le comportement transitoire du GC. Tout ce dont vous avez besoin est le PID du processus. Peut-être que cela vous aidera pendant que vous travaillez sur JProfile.

Et qu'en est-il des aspects Spring tracing/logging? Vous pouvez peut-être écrire un aspect simple, l'appliquer de manière déclarative et faire le profileur d'un pauvre de cette façon.

1
duffymo

"Malheureusement, le problème apparaît également de façon sporadique, il semble être imprévisible, il peut fonctionner pendant des jours ou même une semaine sans aucun problème, ou il peut échouer 40 fois par jour, et la seule chose que je peux sembler attraper de manière cohérente est que la collecte des ordures agit. "

On dirait que cela est lié à un cas d'utilisation qui est exécuté jusqu'à 40 fois par jour, puis plus pendant des jours. J'espère que vous ne faites pas que suivre les symptômes. Cela doit être quelque chose, que vous pouvez affiner en retraçant les actions des acteurs de l'application (utilisateurs, emplois, services).

Si cela se produit par des importations XML, vous devez comparer les données XML du 40 jour de crash avec les données, qui sont importées un jour de crash nul. C'est peut-être une sorte de problème logique, que vous ne trouvez pas uniquement dans votre code.

1
cafebabe