Skip to the content.

Αποδοτική Java - Μέρος 3ο: Εργαλεία κατατομής

© Γιάννης Κωστάρας


<-


Στο πρώτο κεφάλαιο μιλήσαμε για το μοντέλο μνήμης της εικονικής μηχανής Java. Στο δεύτερο κεφάλαιο είδαμε διάφορα εργαλεία που μας βοηθούν να επιβλέψουμε (monitoring) την απόδοση μιας εφαρμογής Java, δηλαδή τη συλλογή δεδομένων, χωρίς επίδραση στην εφαρμογή. Με αυτά τα εργαλεία μπορούμε να ανιχνεύσουμε διαρροές μνήμης (memory leaks), υπερβολική χρήση της Κ.Μ.Ε., προβλήματα που αφορούν επικοινωνία με συσκευές εισόδου/εξόδου κλπ. Αφού καταλήξουμε ότι όντως υπάρχει κάποιο πρόβλημα στην εφαρμογή μας, το επόμενο βήμα είναι να βρούμε ποια(-ες) κλάση(-εις) είναι ο(οι) φταίχτης(-ες). Αυτό το πετυχαίνουμε με τη διαδικασία της κατατομής (profiling).

Σε αυτό το κεφάλαιο θα δούμε εργαλεία profiling με τα οποία μπορούμε να εντοπίσουμε που οφείλονται τα διάφορα προβλήματα της απόδοσης της εφαρμογής μας. Τα εργαλεία κατατομής (profiling) επεμβαίνουν σημαντικά και επηρεάζουν την απόδοση της εφαρμογής ώστε να βρουν τον φταίχτη.

Πολλά από τα εργαλεία του προηγούμενου κεφαλαίου διαθέτουν και δυνατότητες profiling όπως θα δούμε στη συνέχεια.

Εργαλεία κατατομής (profilers)

JMap

Σύνταξη:

$ jmap -<option> <vmid> 
$ jmap -options
-heap
-histo[:live]
-permstat
-finalizerinfo
-dump:<dump-options>
-F
-J<flag>

Από τις παραπάνω παραμέτρους θα δείξουμε μόνο εκείνη που χρησιμοποιείται για την επίβλεψη του σωρού. Λόγω ενός προβλήματος στο Ubuntu, προτού εκτελέσετε την jmap, θα πρέπει να εκτελέσετε την παρακάτω εντολή:

$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
[sudo] password for john:
$ jps -l -m
9953 benchmarks.jar 10792 sun.tools.jps.Jps -l -m 10777 org.openjdk.jmh.runner.ForkedMain 127.0.0.1 
$ jmap -heap 9953
Attaching to process ID 9953, please wait... 
Debugger attached successfully. 
Server compiler detected. 
JVM version is 24.65-b04 

using thread-local object allocation. 
Mark Sweep Compact GC 

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 520093696 (496.0MB)
   NewSize          = 1310720 (1.25MB)
   MaxNewSize       = 17592186044415 MB
   OldSize          = 5439488 (5.1875MB)
   NewRatio         = 2
   SurvivorRatio    = 8
   PermSize         = 21757952 (20.75MB)
   MaxPermSize      = 174063616 (166.0MB)
   G1HeapRegionSize = 0 (0.0MB) 
...

Η παράμετρος heap εμφανίζει μια εποπτική εικόνα της μνήμης σωρού:

και για καθέναν από αυτούς εμφανίζει τη χωρητικότητα (capacity), πόσος χώρος χρησιμοποιείται (used) και πόσος απομένει (free).

Γραφικά εργαλεία κατατομής (GUI)

VisualVM ή JVisualVM

Όπως είδαμε στο προηγούμενο κεφάλαιο, πρόκειται για ένα εργαλείο “για όλες τις δουλειές”. Είναι:

Όπως βλέπουμε στην παρακάτω εικόνα, διαθέτει και μια καρτέλα Profiler η οποία, όπως σωστά μαντέψατε, είναι αυτή που χρειαζόμαστε.

Εικόνα 14 VisualVM – καρτέλα Profiler

Όπως βλέπουμε στην παραπάνω εικόνα, μπορούμε να επιλέξουε ανάμεσα σε profiling για προβλήματα απόδοσης της Κ.Μ.Ε. ή της μνήμης.

Εικόνα 15 VisualVM – καρτέλα Monitor

Εικόνα 16 VisualVM – καρτέλα Threads

Η καρτέλα Threads μας δείχνει την κατάσταση των νημάτων. Ένα νήμα μπορεί να είναι σε μια από τις ακόλουθες καταστάσεις:

Το παράθυρο αυτό χρησιμεύει να δείτε ποια νήματα είναι μπλοκαρισμένα, τι συμβαίνει σε περίπτωση deadlock, συνωστισμού κλειδωμάτων (lock contention) κλπ. Μπορείτε επίσης να δημιουργήσετε ένα thread dump ώστε να δείτε πιο αναλυτικά την κατάσταση των νημάτων σας σε μια δεδομένη στιγμή. Η καρτέλα Visual GC μας δίνει μια ωραία εποπτική εικόνα του σκουπιδιάρη (βλ. εικόνα που ακολουθεί) και πώς αυτός δουλεύει καθώς εκτελείται η εφαρμογή!

Εικόνα 17 VisualVM – καρτέλα Visual GC

Στ’ αριστερά βλέπετε τη Μόνιμη γενιά (Perm) που χρησιμοποιεί η εικονική μηχανή για ν’ αποθηκεύσει τα δικά της αντικείμενα, κλάσεις ή στατικά πεδία. (Η Μόνιμη Γενιά αντικαταστάθηκε από το metaspace στην Java 8, και δεν περιλαμβάνεται στο μέγεθος της μνήμης σωρού). Δεξιότερα είναι η Παλαιά γενιά (Old) στην οποία αποθηκεύονται τα θητεύοντα αντικείμενα. Δεξιότερα είναι η Νέα γενιά που χωρίζεται στις Eden, S0 και S1. Καθώς η εφαρμογή εκτελείται θα παρατηρήσετε την Eden να γεμίζει και στη συνέχεια να εκτελείται ο σκουπιδιάρης της νέας γενιάς που μεταφέρει όσα αντικείμενα είναι ακόμα “ζωντανά” σε κάποιον από τους χώρους επιβίωσης S0 ή S1 και μετά πάλι πίσω στον Eden κ.ο.κ. Όσα αντικείμενα επιβιώνουν μετά από κάποιον αριθμό σαρώσεων (tenuring threshold), μεταφέρονται στην παλαιά γενιά.

Το πρόσθετο Memory Pools (βλ. παρακάτω εικόνα) εμφανίζει τη χρήση των διαφόρων χώρων μνήμης (Eden, Survivor, Perm, Tenured, Code Cache - χρησιμοποιείται από τον μεταγλωττιστή JIT για την αποθήκευση μεταγλωττισμένου κώδικα JIT. Χρήσιμο είναι το εργαλείο JITWatch) σε συνάρτηση με το χρόνο.

Εικόνα 18 – VisualVM – καρτέλα Memory Pools

Με το VisualVM μπορείτε να δημιουργήσετε στιγμιότυπα της εφαρμογής (application snapshots), heap dumps ακόμα και thread dumps. Αλλά γι’ αυτά θα μιλήσουμε στο επόμενο κεφάλαιο. Παρατηρήστε ότι το VisualVM δεν μπορεί ν’ ανοίξει .vgc αρχείο, αλλά μας επιτρέπει να επιβλέπουμε την εφαρμογή μας ενώ αυτή τρέχει.

Java Mission Control

Έρχεται μαζί με την ΕΜ Java της Oracle από την έκδοση 7 update 40 (JDK 7u40) και μετά αλλά όχι με το OpenJDK. Προέρχεται από την ΕΜ JRockit, την ΕΜ της Oracle. Όταν η Oracle εξαγόρασε τη Sun Microsystems, απέκτησε στη διάθεσή της και την ΕΜ Hotspot της Sun. Το αποτέλεσμα ήταν η Oracle να συγχωνεύσει τα καλύτερα χαρακτηριστικά της JRockit στην ΕΜ της Sun. Ένα από αυτά είναι και το Java Mission Control. Για να το εκκινήσετε (από το Oracle JDK κι όχι από το OpenJDK υποθέτοντας ότι το έχετε εγκαταστήσει π.χ. στο παρακάτω φάκελο):

$ /opt/java/jdk1.7.0_71/bin/jmc & 

Απλά επιλέξτε το VMID του προγράμματος που θέλετε να επιβλέψετε από τ’ αριστερό πάνελ, δεξί κλικ και Start JMX Console.

Εικόνα 19 Κονσόλα Java Mission Control – καρτέλα Overview

Πέραν της καρτέλας Overview, ενδιαφέρον έχει για το σκοπό αυτού του άρθρου και η καρτέλα Memory.

Εικόνα 20 Κονσόλα Java Mission Control – καρτέλα Memory

Για να επιβλέψετε κάποιο πρόγραμμα που εκτελείται σε άλλον Η/Υ από αυτόν που εκτελείτε το JMC, θα πρέπει να εκκινήσετε την εφαρμογή σας με τις ακόλουθες παραμέτρους, να ενεργοποιήσετε δηλαδή το Java Descovery Protocol (JDP) ώστε να μπορέσει να ανιχνευθεί από το JMC (φυσικά θα πρέπει να αφήσετε τη θύρα 7091 ανοιχτή από τυχόν τείχος προστασίας (firewall)):

-Dcom.sun.management.jmxremote.port=true
-Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.autodiscovery=true

(βλ. http://hirt.se/blog/?p=388)

Οι παραπάνω 4 παράμετροι είναι οι ίδιες που απαιτούνται και από το VisualVM για να συνδεθεί με μια απομακρυσμένη εφαρμογή. Αν η απομακρυσμένη ΕΜ δεν διαθέτει JDP, ενεργοποιήσετε τον πράκτορα jmxrmi όπως θα κάνατε για να συνδεθείτε με πελάτες JMX (όπως π.χ. το Jconsole). Σημειώστε, ότι η συχνότητα συλλογής δεδομένων είναι πολύ μικρή ώστε η επίδραση της επίβλεψης του JMC στην εφαρμογή να είναι αμελητέα.

JConsole

Πρόκειται για ένα (συμβατό με JMX) εργαλείο, που περιλαμβάνεται στο JDK, το οποίο έχει τις εξής δυνατότητες:

Στην Java 5 πρέπει να περάσετε την εξής παράμετρο για να ενεργοποιήσετε το JConsole:

-Dcom.sun.management.jmxremote

Από τη Java 6 και μετά είναι ενεργοποιημένη εξ’ ορισμού.

$ jconsole &

Η αρχική οθόνη ζητά να ορίσετε μια νέα σύνδεση με μια υπάρχουσα εφαρμογή της οποίας πρέπει να γνωρίζετε το PID (Process ID) αν εκτελείται τοπικά, ή το όνομα του Η/Υ και τη θύρα αν η εφαρμογή εκτελείται απομακρυσμένα. Επειδή το jconsole απαιτεί πολλούς πόρους για να εκτελεστεί, όταν θέλουμε να μετρήσουμε την απόδοση μιας εφαρμογής που τρέχει στο σύστημα παραγωγής, καλό είναι να συνδεόμαστε σ’ αυτό από άλλον Η/Υ για να παράγουμε τις μετρήσεις για να μην επηρεάσουμε την απόδοση της εφαρμογής.

Εικόνα 24 Αρχική οθόνη του JConsole

Η αρχική οθόνη αποτελείται από έξι καρτέλες:

Η πρώτη καρτέλα εμφανίζει μια σύνοψη της εφαρμογής, πιο συγκεκριμένα τη χρήση της μνήμης σωρού, τον αριθμό των νημάτων, τον αριθμό των κλάσεων που φορτώθηκαν και τη χρήση της Κ.Μ.Ε.

Εικόνα 25 JConsole: καρτέλα σύνοψης

Στην καρτέλα μνήμης μπορείτε να επιλέξετε από την πτυσσόμενη λίστα πάνω αριστερά ποιο γράφημα θέλετε ν’ απεικονίσετε: μνήμη σωρού, μνήμη στοίβας, παλαιά γενιά, Εδέμ, χώρο επιβίωσης, θητεύοντα αντικείμενα κλπ. Επιβλέποντας αυτά τα γραφήματα μπορεί ν’ ανακαλύψετε μια διαρροή μνήμης. Μια σύνοψη της χρήσης μνήμης καθώς και των συλλεκτών σκουπιδιών που χρησιμοποιήθηκαν εμφανίζεται στο κάτω μέρος της καρτέλας.

Εικόνα 26 JConsole: καρτέλα μνήμης

Στην καρτέλα διεργασιών βλέπουμε τον αριθμό των διεργασιών (νημάτων) που εκτελούνται καθώς και το μέγιστο αριθμό τους. Στο κάτω μέρος της καρτέλας βλέπουμε αναλυτικά όλες τις διεργασίες, ενώ επιλέγοντας μια βλέπουμε την κατάστασή της. Μας δίνεται ακόμα η δυνατότητα ν’ ανιχνεύσουμε deadlocks (που ξεφεύγει από το σκοπό τούτου του άρθρου).

Εικόνα 27 JConsole: καρτέλα διεργασιών

Η επόμενη καρτέλα απεικονίζει τον αρ. των κλάσεων που έχουν φορτωθεί.

Εικόνα 28 JConsole: καρτέλα κλάσεων

Η επόμενη καρτέλα απεικονίζει πληροφορίες της εικονικής μηχανής. Το Total compile time εμφανίζει το χρόνο που ξοδεύτηκε για μεταγλώττιση just-in-time (JIT). Η υλοποίηση της JVM καθορίζει πότε θα συμβεί μεταγλώττιση JIT. Η Hotspot VM χρησιμοποιεί προσαρμοστική μεταγλώττιση, κατά την οποία η εικονική μηχανή φορτώνει την εφαρμογή χρησιμοποιώντας έναν απλό διερμηνευτή, αλλά στη συνέχεια αναλύει τον κώδικα καθώς αυτός εκτελείται για ν’ ανιχνεύσει προβλήματα απόδοσης ή “hot spots”.

Εικόνα 29 JConsole: καρτέλα JVM

Η τελευταία καρτέλα εμφανίζει ένα δέντρο με τα διάφορα MBeans που παρέχονται από τον διακομιστή MBeans της πλατφόρμας. Επιλέγοντας ένα MBean από το δέντρο εμφανίζονται οι ιδιότητές του, ενώ όσες τιμές εμφανίζονται με μπλε μπορείτε να τις αλλάξετε.

Εικόνα 30 JConsole: καρτέλα MBeans

Επίλογος

Στο 3ο και τελευταίο μέρος είδαμε διάφορα εργαλεία κατατομής (profiling) που σας επιτρέπουν να βρείτε τι φταίει με την απόδοση μιας εφαρμογής Java. Σας βοηθούν να ανιχνεύσετε που οφείλονται (βλ. ποιες κλάσεις είναι υπεύθυνες για) διαρροές μνήμης, εκτεταμένη χρήση της Κ.Μ.Ε. (CPU) και άλλα προβλήματα που σας βοηθούν να συντονίσετε την ΕΜ σας για μέγιστη απόδοση. Είδαμε πως μπορούμε να χρησιμοποιήσουμε τα εργαλεία αυτά για να βρούμε σε ποιο σημείο του κώδικά μας υπάρχει π.χ. διαρροή μνήμης, ή συνωστισμός κλειδωμάτων κλπ. ώστε να μπορέσουμε να τα διορθώσουμε.

Πηγές:

  1. Hunt C. & Binu J. (2012), Java Performance, Addison-Wesley.
  2. Evans, B. & Verburg M. (2012), The Well Grounded Java Developer, Manning.
  3. Pepperdine, K. (2010), “Performance Tuning with Cheap Drinks and Poor Tools”.
  4. Tene G. (2011) “Understanding Java Garbage Collection and What You Can Do about It”.
  5. RR’s Random Ramblings (2012), “Java Tuning in a Nutshell – Part 1”.
  6. Thompson, M. (2013), “Java Garbage Collection Distilled”, InfoQ.
  7. Shirazi J. (2012), “Garbage Collectors available in JDK 1.7.0_04”.
  8. Lee, S. (2012), “Understanding Java Garbage Collection”.
  9. Lee, S. (2012), “How to monitor Java Garbage Collection”.
  10. Lee, S. (2012), “How to tune Java Garbage Collection”.
  11. Warburton, R. (2013), “Garbage Collection in Java (1)”.
  12. Warburton, R. (2013), “Garbage Collection in Java (2)”.
  13. Warburton, R. (2013), “Garbage Collection in Java (3)”.
  14. Warburton, R. (2013), “Garbage Collection in Java (4)”.
  15. Steingarten N. (2013), “JVM Performance Magic Tricks”.
  16. Java HotSpot VM Options.
  17. Java Platform, Standard Edition Troubleshooting Guide.
  18. Perera I. (2016), Using Java Flight Recorder.
  19. Vorontsov M. (2015), [Oracle Java Mission Control: The Ultimate Guide] (https://blog.takipi.com/oracle-java-mission-control-the-ultimate-guide/).
  20. Analyzing Memory Leak in Java Applications using VisualVM

<-