Accueil Nos publications Blog Java Performances (2/3) – Dumps mémoire

Java Performances (2/3) – Dumps mémoire

Lorsque l’analyse de la configuration de la JVM et de son exécution sur un serveur ne permet pas d’améliorer les performances, il est nécessaire d’analyser les différents threads et la heap.

Cet article appartient à la suite d’article (Configuration de la JVM, Dumps mémoire et Optimisation de code) qui a pour objectif  de présenter les différentes étapes d’analyse d’un problème de performances d’une application en production sous un Système d’Exploitation de type Unix, en utilisant une approche bottom-up et ceci en nous focalisant uniquement au niveau de la JVM.

Thread Dump

Si une application est plus lente que prévue, ou semble bloquée, voire si une exception de type StackOverflowException est levée, un Thread Dump permet d’identifier un problème potentiel. Les problèmes les plus courants sont détaillés dans la section du même nom.

Il est possible qu’une exception StackOverflowException soit levée, sans qu’il y ait une erreur dans l’implémentation. Néamoins, avant de modifier la taille des stacks, il est nécessaire de passer par une phase d’optimisation du code. Gardez à l’esprit que l’augmentation de la stack d’un thread, impacte toutes les autres stacks, et donc les performances générales de l’application.

Etat d’un thread

Fig. 1) Thread State Diagram
  • NEW  : Le Thread a été créé mais n’a pas encore été exécuté.
  • RUNNABLE  : Le Thread est en train d’exécuter une tâche.
  • BLOCKED  : Le Thread attend qu’un autre thread libère un lock pour qu’il puisse le prendre.
  • WAITING  : Le thread est en attente d’un événement. Cet état est initié par une des méthodes suivantes : wait , join ou park .
  • TIMED_WAITING  : Le thread est en attente. Contrairement à l’état WAITING, TIMED_WAITING a une durée maximale d’attente. Par conséquent, le thread peut passer à l’état RUNNABLE aussi bien par un événement externe, que par l’écoulement de la durée du timer.
  • TERMINATED : L’exécution du thread est achevée.

Cet état est initié par une des méthodes suivantes : sleep , wait , join ou park .

Stack Trace

Un Full Thread Dump de la JVM est une image à un instant T de tous les threads actifs.

Nous pouvons réaliser un « Full Thread Dump » à l’aide de l’outil jstack [1].


        # javaw -Xms256m –Xmx1024m com.soat.perf.SoatPerf
        # jps
        3792 Jps
        4484 SoatPerf
        # jstack 4484
        2012-10-31 16:07:48
        Full thread dump Java HotSpot(TM) 64-Bit Server VM (20.5-b03 mixed mode):
        (...)

        "Finalizer" daemon prio=8 tid=0x0000000000528800 nid=0xde0 in Object.wait() [0x000000000669f000]
        java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on  (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(Unknown Source)
        - locked  (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(Unknown Source)
        at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source)

        "Reference Handler" daemon prio=10 tid=0x0000000000526000 nid=0x778 in Object.wait() [0x000000000659f000]
        java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on  (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:485)
        at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source)
        - locked  (a java.lang.ref.Reference$Lock)

        "main" prio=6 tid=0x00000000003db000 nid=0xc9c runnable [0x000000000252f000]
        java.lang.Thread.State: RUNNABLE
        at com.soat.perf.SoatPerf.main(SoatPerf.java:19)

        "VM Thread" prio=10 tid=0x000000000051d800 nid=0xe30 runnable

        (...)

        JNI global references: 886
        
Fig. 2) Full Thread Dump. (Pour des raisons de place, le dump a été tronqué).

Pour chaque thread les informations suivantes sont données :

  • Le nom du Thread
  • Le type et la priorité du thread – ex:  < daemon prio=10 >
  • L’ID du Thread – ex: < tid=0x00000000003db000 >. Il s’agit de l’id du Thread Java obtenu via java.lang.Thread.getId().
  • L’ID natif du Thread   – ex: < nid=0xc9c >. Il s’agit de l’id du thread au niveau du Système d’Exploitation.
  • L’état du Thread – ex: < java.lang.Thread.State: RUNNABLE > – Permet d’identifier de potentiels problèmes de lock.
  • La Stack Trace du Thread

Types de Thread

Il existe deux types de Thread Java :

  • Les Threads de type daemon
  • Et les Threads de type non-daemon

Les threads de type daemon s’arrêtent lorsqu’il n’y a plus de threads de type non-daemon.  Même si une application ne crée pas de threads, la JVM en crée plusieurs, dont la plus part sont des threads de type daemon.

Le thread principal d’une application est celui exécuté la méthode main . Il est le premier et le dernier thread de type non-daemon à s’exécuter. Lorsque l’on atteint la fin de la méthode main, le thread se termine, mettant fin aux threads de type non-daemon et par conséquent à l’exécution de la JVM.

A noter qu’un thread de type daemon peut être créé au sein d’une application.

Problèmes courant

Lock impossible

Un thread à l’état RUNNABLE maintient le lock sur une ressource (les raisons pouvant être multiples), rendant impossible la prise de lock par un autre thread, qui reste à l’état BLOCKED. Ce type d’erreur peut intervenir si une méthode complète est synchonized. Par conséquent, il est préférable de définir des blocks synchronisés atomiques, c’est-à-dire  lockant uniquement les ressources à synchroniser.

Deadlock

Probablement le problème de concurrence le plus connu. Deux threads ont un lock sur deux ressources différentes. Pour poursuivre leur exécution chacun d’eux a besoin d’obtenir le lock sur la ressource lockée par l’autre. Les deux threads restent donc indéfiniment à l’état BLOCKED .

jstack permet d’identifier les deadlocks :


    (...)
    Found one Java-level deadlock:
    =============================
    "Thread-1":
    waiting to lock monitor 0x000000000052ddc0 (object 0x00000000e69627a0, a java.lang.Object),
    which is held by "Thread-0"
    "Thread-0":
    waiting to lock monitor 0x000000000052f170 (object 0x00000000e69627b0, a java.lang.Object),
    which is held by "Thread-1"

    Java stack information for the threads listed above:
    ===================================================
    "Thread-1":
    at com.soat.perf.DeadLock.lock21(DeadLock.java:45)
    - waiting to lock  (a java.lang.Object)
    - locked  (a java.lang.Object)
    at com.soat.perf.DeadLock.access$100(DeadLock.java:6)
    at com.soat.perf.DeadLock$2.run(DeadLock.java:25)
    at java.lang.Thread.run(Unknown Source)
    "Thread-0":
    at com.soat.perf.DeadLock.lock12(DeadLock.java:36)
    - waiting to lock  (a java.lang.Object)
    - locked  (a java.lang.Object)
    at com.soat.perf.DeadLock.access$000(DeadLock.java:6)
    at com.soat.perf.DeadLock$1.run(DeadLock.java:20)
    at java.lang.Thread.run(Unknown Source)

    Found 1 deadlock.
    
Fig. 3) Deadlock identifié par jstack

En cours d’exécution, mais en attente

Un thread peut être à l’état RUNNABLE , mais dont l’exécution est dans une boucle infinie (volontaire ou non). Une boucle -à première vue infinie ( for(;;) ou while(true) )- est volontaire lorsque l’on espère qu’un évènement provoquera une sortie de cette boucle. Ceci est notamment le cas lorsqu’une application est en attente d’un input utilisateur ou réseau.

Attente infinie

Un thread peut être indéfiniment à l’état WAITING  si  aucun événement ne le réveille. Ce qui peut être le résultat d’une erreur dans l’implémentation du thread ou du waker.

Thread Pool

Il est à noter que, généralement, pour des raisons de performances les serveurs d’applications (et conteneur de servlets), créent lors de leur initialisation un (ou plusieurs) pool de Threads.

Ceci à deux avantages :

  • Limiter le nombre de threads. Le serveur alloue un thread libre, s’il y en a un, à chaque requête ou place la requête dans une file d’attente.
  • Eviter de créer un thread à chaque requête, ce qui a un coût en termes de temps quantum.

Heap Dump

Lorsqu’une application crashe en levant des exceptions de type OutOfMemoryError (Java heap space ou PermGen Space) avant d’augmenter la taille de la heap ou de la Permanent Generation Space, il est conseillé d’effectuer un dump de la heap. De même, si une application est lente, il est possible qu’une zone mémoire soit trop rapidement saturée, ce qui entraine de multiple “garbage collections” et donc l’exécution des threads associés, augmentant le nombre de cycles CPU entre chaques cycles alloués à cette application, ce qui, -sans surprise-, rendent son exécution plus lente.

La Heap est partagée entre tous les threads d’une JVM. La heap est la zone mémoire dans laquelle toutes les instances de classes et d’arrays sont stockées.

Dumper la heap nous permet d’identifier d’éventuelles fuites mémoires ou une mauvaise optimisation du code, par exemple, lorsqu’un objet est instancié des milliers de fois et jamais « garbage collecté ».

L’outil fourni avec la JDK permettant de dumper la heap est jmap [2]. Pour exploiter ce dump, nous pouvons utiliser jhat [3].


        # javaw com.soat.perf.SoatPerf
        # jps
        3792 Jps
        4484 SoatPerf
        # jmap -dump:file=heap_dump.bin 4484
        Dumping heap to /home/heap_dump/heap_dump.bin ...
        Heap dump file created
        # jhat heap_dump.bin
        Reading from heap_dump.bin...
        Dump file created Mon Nov 05 10:57:14 CET 2012
        Snapshot read, resolving...
        Resolving 5041 objects...
        Chasing references, expect 1 dots.
        Eliminating duplicate references.
        Snapshot resolved.
        Started HTTP server on port 7000
        Server is ready.
        
Fig. 4) Création d’un dump et exploitation avec jhat

 

Lorsque le serveur de jhat est lancé, il suffit d’ouvrir un navigateur et de saisir l’adresse suivante : https://127.0.0.1:7000

Fig. 5) Détail de la heap avec jhat

En cliquant sur le lien « Show instance counts for all classes (including platform) » nous obtenons les classes instanciées et le nombre d’instances pour chacunes d’entre elles. :

Fig. 6) Liste des classes instanciées, ordonnées par leur nombre d’instances

A noter que les classes commençant par « [ » sont des arrays, comme indiqué dans la spécification de la JVM [4].

Les liens « instances » et « class <type> » affichent respectivement une liste de toutes les instances du type en question et le détail de la classe (les méta-données) ainsi que ses différentes instances.

Sur la page d’accueil, le lien « Show heap histogram » affiche une liste des différentes classes instanciées, ordonnées par l’espace mémoire qu’elles occupent.

 

Fig. 7) Liste des différentes classes instanciées, ordonnées par taille

Auto-Dump

Lorsqu’une application a tendance à lever des exceptions de type « OutOfMemory », il est possible d’indiquer à la JVM lors de son initialisation, qu’elle doit créer un dump de la heap si une erreur de ce type apparaît, avant de s’arrêter.


        # java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/heap_dump/ com.soat.perf.SoatPerf
        java.lang.OutOfMemoryError: Java heap space
        Dumping heap to /home/heap_dump/java_pid2116.hprof ...
        Heap dump file created [898606200 bytes in 16.507 secs]
        Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.Arrays.copyOf(Unknown Source)
        at java.util.ArrayList.ensureCapacity(Unknown Source)
        at java.util.ArrayList.add(Unknown Source)
        at com.soat.perf.SoatPerf.main(SoatPerf.java:20)
        
Fig. 8) La heap est dumpée si une exception de type OutOfMemory est levée.

Le nom du fichier est le suivant : java_pid<lvmid>.hprof où < lvmid > est l’id de la JVM (le pid).

Le lancement de jhat s’effectue de la même manière que précédemment :


    # jhat -J-mx3072m /home/heap_dump/java_pid2116.hprof
    Reading from /home/heap_dump/java_pid2116.hprof...
    Dump file created Mon Nov 05 11:26:17 CET 2012
    Snapshot read, resolving...
    Resolving 3931 objects...
    Chasing references, expect 0 dots
    Eliminating duplicate references
    Snapshot resolved.
    Started HTTP server on port 7000
    Server is ready.
    
Fig. 9) Exploitation d’un fichier de type hprof.

Il est important de noter que jhat a besoin de beaucoup de mémoire pour générer les différents fichiers. Par conséquent, pour éviter un OutOfMemoryException il est possible de fournir en paramètre -J-mx<taille>m .

Core Dump

Utiliser les différents outils fournis avec la JDK pour effectuer un dump de la heap ou de la stack peut prendre beaucoup de temps et de mémoire, jusqu’à consommer la majorité des ressources. Le core dump est donc une solution.

De plus, malgré quelques contraintes, un core dump permet une certaine souplesse inexistante en runtime, mais aussi de faire du profiling sur son poste en local.

Outils

L’outil le plus communément utilisé est gcore [5]. Il a l’avantage de ne pas arrêter le processus à dumper.


        gcore 
        
Fig. 10) Création d’un dump pour un pid donné.

Cette commande génère un fichier nommé «  core .<lvmid> » où <lvmid> est l’id de la JVM (le pid).

Les contraintes

Pour pouvoir exploiter un fichier de dump il est indispensable d’avoir, en local, exactement la même version de l’outil qui a créé le dump.

Créer un dump peut nécessiter beaucoup de ressources, ce qui peut être problématique sur un environnement de production.

Un dump ayant la taille de la mémoire utilisée par un processus (et non pas allouée), s’il utilise 32 Go, il créera un fichier de 32 Go.

De plus, il est à noter que si le processus n’est pas complétement suspendu, le dump sera en partie inconsistant. Pour éviter ce problème, il  est possible d’utiliser l’option -s de gcore .

Exploitation d’un Core Dump

Explorer un dump n’a que très peu d’intérêt. Il est plus courant de l’exploiter avec les outils présentés précédemment.

Configuration Info

Jinfo peut extraire les informations d’un dump en indiquant l’exécutable ayant créé le dump et le nom du fichier de dump.


        # javaw com.soat.perf.SoatPerf
        # jps
        3792 Jps
        4484 SoatPerf
        # gcore 4484
        # jinfo gcore core.4484
        (... idem que Fig. 3 [Java Performances - Configuration de la JVM] ...)
        
Fig. 11)  Propriétés système et flags d’une JVM  depuis un dump.

Memory Map/Stack Trace

jmap et jstack fonctionnent de la même manière que jinfo .


        # jmap gcore core.4484
        
Fig. 7) Exploitation d’un core dump avec jmap

        # jstack gcore core.4484
        
Fig. 12) Exploitation d’un core dump avec jstack .

 Java Heap Analysis Tool

En revanche, pour exploiter un core dump avec jhat (comme avec d’autres outils), il est nécessaire de le convertir.


        # jmap -dump:format=b,file=dump.hprof gcore core.4484
        Attaching to core core.4484 from executable gcore, please wait...
        Debugger attached successfully.
        Server compiler detected.
        JVM version is 20.5-b03
        Dumping heap to dump.hprof ...
        Finding object size using Printezis bits and skipping over...
        Heap dump file created
        # jhat -J-mx3072m dump.hprof
        (...)
        
Fig. 13) Conversion d’un core dump avec jmap et exploitation avec jhat .

Ressources

[1] https://docs.oracle.com/javase/7/docs/technotes/tools/share/jstack.html

[2] https://docs.oracle.com/javase/7/docs/technotes/tools/share/jmap.html

[3] https://docs.oracle.com/javase/7/docs/technotes/tools/share/jhat.html

[4] https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3

[5] https://www.lehman.cuny.edu/cgi-bin/man-cgi?gcore