Προγραμματισμός

Διάγνωση συνηθισμένων προβλημάτων χρόνου εκτέλεσης με το hprof

Διαρροές μνήμης και αδιέξοδα και γουρούνια CPU, ωχ! Οι προγραμματιστές εφαρμογών Java αντιμετωπίζουν συχνά αυτά τα προβλήματα χρόνου εκτέλεσης. Μπορούν να είναι ιδιαίτερα τρομακτικές σε μια πολύπλοκη εφαρμογή με πολλά νήματα που διατρέχουν εκατοντάδες χιλιάδες γραμμές κώδικα - μια εφαρμογή που δεν μπορείτε να στείλετε επειδή μεγαλώνει στη μνήμη, καθίσταται ανενεργή ή καταβροχθίζει περισσότερους κύκλους CPU από ό, τι θα έπρεπε.

Δεν είναι μυστικό ότι τα εργαλεία δημιουργίας προφίλ Java είχαν πολύ δρόμο να καλύψουν τους αντίστοιχους εναλλακτικούς. Υπάρχουν πολλά ισχυρά εργαλεία για να μας βοηθήσουν να εντοπίσουμε τους ενόχους πίσω από αυτά τα κοινά προβλήματα. Αλλά πώς αναπτύσσετε την εμπιστοσύνη στην ικανότητά σας να χρησιμοποιείτε αυτά τα εργαλεία αποτελεσματικά; Μετά από όλα, χρησιμοποιείτε τα εργαλεία για τη διάγνωση περίπλοκων συμπεριφορών που δεν καταλαβαίνετε. Για να επιδεινώσετε την κατάσταση σας, τα δεδομένα που παρέχονται από τα εργαλεία είναι λογικά περίπλοκα και οι πληροφορίες που αναζητάτε ή δεν είναι πάντα σαφείς.

Όταν αντιμετώπισα παρόμοια προβλήματα στην προηγούμενη ενσάρκωσή μου ως πειραματικός φυσικός, δημιούργησα πειράματα ελέγχου με προβλέψιμα αποτελέσματα. Αυτό με βοήθησε να αποκτήσω εμπιστοσύνη στο σύστημα μέτρησης που χρησιμοποίησα σε πειράματα που δημιούργησαν λιγότερο προβλέψιμα αποτελέσματα. Ομοίως, αυτό το άρθρο χρησιμοποιεί το εργαλείο προφίλ προφίλ hprof για να εξετάσει τρεις απλές εφαρμογές ελέγχου που παρουσιάζουν τις τρεις συνήθεις προβληματικές συμπεριφορές που αναφέρονται παραπάνω. Αν και δεν είναι τόσο φιλικό προς το χρήστη όσο ορισμένα εμπορικά εργαλεία στην αγορά, το hprof περιλαμβάνεται στο Java 2 JDK και, όπως θα δείξω, μπορεί να διαγνώσει αποτελεσματικά αυτές τις συμπεριφορές.

Εκτελέστε με hprof

Η εκτέλεση του προγράμματός σας με hprof είναι εύκολη. Απλώς επικαλέστε το Java runtime με την ακόλουθη επιλογή γραμμής εντολών, όπως περιγράφεται στην τεκμηρίωση εργαλείου JDK για την εφαρμογή εκκίνησης Java:

java -Xrunhprof [: help] [: =, ...] MyMainClass 

Μια λίστα με τις επιλογές είναι διαθέσιμη με το [:βοήθεια] εμφανίζεται η επιλογή. Δημιούργησα τα παραδείγματα σε αυτό το άρθρο χρησιμοποιώντας τη θύρα Blackdown του JDK 1.3-RC1 για Linux με την ακόλουθη εντολή εκκίνησης:

java -classic -Xrunhprof: heap = sites, cpu = δείγματα, βάθος = 10, οθόνη = y, νήμα = y, doe = y MemoryLeak 

Η ακόλουθη λίστα εξηγεί τη λειτουργία κάθε υποεπιλογής που χρησιμοποιήθηκε στην προηγούμενη εντολή:

  • σωρός = ιστότοποι: Λέει στο hprof να δημιουργήσει ίχνη στοίβας που υποδεικνύουν πού εκχωρήθηκε η μνήμη
  • cpu = δείγματα: Λέει στην hprof να χρησιμοποιήσει στατιστικά δείγματα για να προσδιορίσει πού περνά ο χρόνος της CPU
  • βάθος = 10: Λέει στο hprof να δείξει ίχνη στοίβας σε βάθος 10 επιπέδων, το πολύ
  • οθόνη = y: Ζητά από την hprof να δημιουργήσει πληροφορίες για οθόνες διαμάχης που χρησιμοποιούνται για τον συγχρονισμό της εργασίας πολλών νημάτων
  • νήμα = y: Λέει στο hprof να αναγνωρίσει νήματα σε ίχνη στοίβας
  • doe = ε: Ζητά από την hprof να δημιουργήσει μια απόρριψη δεδομένων προφίλ κατά την έξοδο

Εάν χρησιμοποιείτε το JDK 1.3, πρέπει να απενεργοποιήσετε τον προεπιλεγμένο μεταγλωττιστή HotSpot με το -κλασσικός επιλογή. Το HotSpot έχει το δικό του προφίλ, το οποίο καλείται μέσω ενός -Χροφ επιλογή, που χρησιμοποιεί μια μορφή εξόδου διαφορετική από αυτήν που θα περιγράψω εδώ.

Η εκτέλεση του προγράμματός σας με hprof θα αφήσει ένα αρχείο που ονομάζεται java.hprof.txt στον κατάλογο εργασίας σας. Αυτό το αρχείο περιέχει τις πληροφορίες προφίλ που συλλέγονται κατά την εκτέλεση του προγράμματος. Μπορείτε επίσης να δημιουργήσετε μια απόρριψη ανά πάσα στιγμή, ενώ το πρόγραμμά σας εκτελείται πατώντας Ctrl- \ στο παράθυρο της κονσόλας Java στο Unix ή Ctrl-Break στα Windows.

Ανατομία ενός αρχείου εξόδου hprof

Ένα αρχείο εξόδου hprof περιλαμβάνει ενότητες που περιγράφουν διάφορα χαρακτηριστικά του προγράμματος Java με προφίλ. Ξεκινά με μια κεφαλίδα που περιγράφει τη μορφή της, την οποία η επικεφαλίδα ισχυρίζεται ότι μπορεί να αλλάξει χωρίς προειδοποίηση.

Οι ενότητες Thread and Trace του αρχείου εξόδου σάς βοηθούν να καταλάβετε ποια νήματα ήταν ενεργά όταν εκτελέστηκε το πρόγραμμά σας και τι έκαναν. Η ενότητα Νήμα παρέχει μια λίστα με όλα τα νήματα που ξεκίνησαν και τερματίστηκαν κατά τη διάρκεια του προγράμματος. Η ενότητα Trace περιλαμβάνει μια λίστα με αριθμημένα ίχνη στοίβας για ορισμένα νήματα. Αυτοί οι αριθμοί ιχνών στοίβας παραπέμπουν σε άλλες ενότητες αρχείων.

Οι ενότητες Heap Dump and Sites σας βοηθούν να αναλύσετε τη χρήση μνήμης. Εξαρτάται από σωρός υποεπιλογή που επιλέγετε όταν ξεκινάτε την εικονική μηχανή (VM), μπορείτε να πάρετε μια απόρριψη όλων των ζωντανών αντικειμένων στο σωρό Java (σωρός = απόρριψη) ή / και μια ταξινομημένη λίστα τοποθεσιών κατανομής που προσδιορίζει τα πιο διατεταγμένα αντικείμενα (σωρός = ιστότοποι).

Οι ενότητες CPU Samples και CPU Time σας βοηθούν να κατανοήσετε τη χρήση της CPU. η ενότητα που λαμβάνετε εξαρτάται από τη δική σας ΕΠΕΞΕΡΓΑΣΤΗΣ υποεπιλογή (cpu = δείγματα ή cpu = ώρα). Τα δείγματα CPU παρέχουν ένα προφίλ στατιστικής εκτέλεσης. Ο Χρόνος CPU περιλαμβάνει μετρήσεις πόσες φορές κλήθηκε μια δεδομένη μέθοδος και πόσο καιρό χρειάστηκε να εκτελεστεί κάθε μέθοδος.

Οι ενότητες Monitor Time and Monitor Dump σάς βοηθούν να κατανοήσετε πώς ο συγχρονισμός επηρεάζει την απόδοση του προγράμματος. Ο Χρόνος παρακολούθησης δείχνει πόση ώρα αντιμετωπίζουν τα νήματα για διαμάχες για κλειδωμένους πόρους. Το Monitor Dump είναι ένα στιγμιότυπο των οθονών που χρησιμοποιούνται αυτήν τη στιγμή. Όπως θα δείτε, το Monitor Dump είναι χρήσιμο για την εύρεση αδιεξόδων.

Διάγνωση διαρροής μνήμης

Στην Java, ορίζω μια διαρροή μνήμης ως (συνήθως) ακούσια αποτυχία αποπροσανατολισμού απορριμμένων αντικειμένων, έτσι ώστε ο συλλέκτης απορριμμάτων να μην μπορεί να ανακτήσει τη μνήμη που χρησιμοποιούν. ο Ελλειψη μνήμης Το πρόγραμμα στην Λίστα 1 είναι απλό:

Λίστα 1. Πρόγραμμα MemoryLeak

01 εισαγωγή java.util.Vector; 02 03 δημόσια τάξη MemoryLeak {04 05 public static void main (String [] args) {06 07 int MAX_CONSUMERS = 10000; 08 int SLEEP_BETWEEN_ALLOCS = 5; 09 10 ConsumerContainer objectHolder = νέο ConsumerContainer (); 11 12 while (objectHolder.size () <MAX_CONSUMERS) {13 System.out.println ("Αντικείμενο κατανομής" + 14 Integer.toString (objectHolder.size ()) 15); 16 objectHolder.add (νέο MemoryConsumer ()); 17 δοκιμάστε το {18 Thread.currentThread (). Sleep (SLEEP_BETWEEN_ALLOCS); 19} catch (InterruptException δηλαδή) {20 // Μην κάνετε τίποτα. 21} 22} // ενώ. 23} // κύρια. 24 25} // Τέλος του MemoryLeak. 26 27 / ** Ονομαζόμενη κλάση κοντέινερ για την κατοχή αναφορών αντικειμένων * / 28 Η κατηγορία ConsumerContainer επεκτείνει το Vector {} 29 30 / ** Class που καταναλώνει ένα σταθερό ποσό μνήμης. * / 31 κλάση MemoryConsumer {32 public static final int MEMORY_BLOCK = 1024; 33 δημόσια byte [] memoryHoldingArray; 34 35 MemoryConsumer () {36 memoryHoldingArray = νέο byte [MEMORY_BLOCK]; 37} 38} // Τελικό MemoryConsumer. 

Όταν το πρόγραμμα εκτελείται, δημιουργεί ένα ConsumerContainer αντικείμενο και μετά αρχίζει να δημιουργεί και να προσθέτει Μνήμη αντικειμένου μεγέθους τουλάχιστον 1 KB σε αυτό ConsumerContainer αντικείμενο. Διατηρώντας τα αντικείμενα προσβάσιμα τα καθιστά μη διαθέσιμα για συλλογή απορριμμάτων, προσομοιώνοντας διαρροή μνήμης.

Ας δούμε επιλεγμένα τμήματα του αρχείου προφίλ. Οι πρώτες γραμμές του τμήματος Ιστότοποι δείχνουν με σαφήνεια τι συμβαίνει:

SITES BEGIN (παραγγελία με ζωντανή bytes) Δευ 3 Σεπ 19:16:29 2001 τοις εκατό ζωντανή κατανομή στοίβας τάξη τάξη αυτοαυξητικά bytes objs bytes objs όνομα ίχνους 1 97.31% 97.31% 10280000 10000 10280000 10000 1995 [B 2 0.39% 97.69% 40964 1 81880 10 1996 [Λ; 3 0,38% 98,07% 40000 10000 40000 10000 1994 Μνήμη 4 0,16% 98,23% 16388 1 16388 1 1295 [C 5 0,16% 98,38% 16388 1 16388 1 1304 [Γ ... 

Υπάρχουν 10.000 αντικείμενα τύπου ψηφιόλεξη[] ([ΣΙ σε VM-speak) καθώς και 10.000 Μνήμη αντικείμενα. Οι συστοιχίες byte καταλαμβάνουν 10.280.000 byte, οπότε προφανώς υπάρχει εναέρια πάνω από τα ακατέργαστα byte που καταναλώνει κάθε συστοιχία. Δεδομένου ότι ο αριθμός των αντικειμένων που κατανέμονται ισούται με τον αριθμό των ζωντανών αντικειμένων, μπορούμε να συμπεράνουμε ότι κανένα από αυτά τα αντικείμενα δεν θα μπορούσε να συλλεχθεί σκουπίδια. Αυτό συνάδει με τις προσδοκίες μας.

Ένα άλλο ενδιαφέρον σημείο: η μνήμη που αναφέρεται ότι καταναλώθηκε από το Μνήμη Τα αντικείμενα δεν περιλαμβάνουν τη μνήμη που καταναλώνεται από τις συστοιχίες byte. Αυτό δείχνει ότι το εργαλείο δημιουργίας προφίλ δεν εκθέτει ιεραρχικές σχέσεις περιορισμού, αλλά μάλλον στατιστικά κατά κατηγορία. Αυτό είναι σημαντικό να γίνει κατανοητό όταν χρησιμοποιείτε το hprof για να εντοπίσετε μια διαρροή μνήμης.

Τώρα, από πού προέρχονται αυτές οι διαρροές byte array; Παρατηρήστε ότι το Μνήμη αντικείμενα και ίχνη αναφοράς συστοιχιών byte 1994 και 1995 στην ακόλουθη ενότητα Trace. Κοιτάξτε, αυτά τα ίχνη μας λένε ότι το Μνήμη αντικείμενα δημιουργήθηκαν στο Ελλειψη μνήμης της τάξης κύριος() μέθοδος και ότι οι συστοιχίες byte δημιουργήθηκαν στον κατασκευαστή (() μέθοδος στο VM-speak). Βρήκαμε διαρροή μνήμης, αριθμούς γραμμής και όλα:

TRACE 1994: (νήμα = 1) MemoryLeak.main (MemoryLeak.java:16) TRACE 1995: (νήμα = 1) MemoryConsumer. (MemoryLeak.java:36) MemoryLeak.main (MemoryLeak.java:16) 

Διάγνωση γουρούνι CPU

Στη λίστα 2, α Απασχολημένος Η τάξη έχει κάθε κλήση νήματος μια μέθοδο που ρυθμίζει πόσο λειτουργεί το νήμα μεταβάλλοντας τον χρόνο ύπνου του μεταξύ των περιόδων εκτέλεσης υπολογισμών εντατικής CPU:

Λίστα 2. Πρόγραμμα CPUHog

01 / ** Κύρια τάξη για δοκιμή ελέγχου. * / 02 δημόσια τάξη CPUHog {03 public static void main (String [] args) {04 05 Thouch slouch, workingStiff, workaholic; 06 slouch = νέο Slouch (); 07 workingStiff = νέο WorkingStiff (); 08 workaholic = νέο Workaholic (); 09 10 slouch.start (); 11 workingStiff.start (); 12 workaholic.start (); 13} 14} 15 16 / ** Χαμηλό νήμα χρήσης CPU. * / 17 Η κατηγορία Slouch επεκτείνει το νήμα {18 δημόσιο Slouch () {19 super ("Slouch"). 20} 21 δημόσια άκυρη εκτέλεση () {22 BusyWork.slouch (); 23} 24} 25 26 / ** Νήμα μέσης χρήσης CPU. * / 27 Η κλάση WorkingStiff επεκτείνει το νήμα {28 public WorkingStiff () {29 super ("WorkingStiff"). 30} 31 δημόσια άκυρη εκτέλεση () {32 BusyWork.workNormally (); 33} 34} 35 36 / ** Υψηλό νήμα χρήσης CPU. * / 37 Η τάξη Workaholic επεκτείνει το νήμα {38 δημόσια Workaholic () {39 super ("Workaholic"). 40} 41 δημόσια άκυρη εκτέλεση () {42 BusyWork.workTillYouDrop (); 43} 44} 45 46 / ** Κατηγορία με στατικές μεθόδους για την κατανάλωση διαφόρων ποσών 47 * χρόνου CPU. * / 48 τάξη BusyWork {49 50 public static int callCount = 0; 51 52 δημόσιος στατικός κενός άσχετος () {53 int SLEEP_INTERVAL = 1000; 54 computeAndSleepLoop (SLEEP_INTERVAL); 55} 56 57 δημόσιο στατικό κενό εργασίας Κανονικά () {58 int SLEEP_INTERVAL = 100; 59 computeAndSleepLoop (SLEEP_INTERVAL); 60} 61 62 δημόσια στατικά άκυρα workTillYouDrop () {63 int SLEEP_INTERVAL = 10; 64 computeAndSleepLoop (SLEEP_INTERVAL); 65} 66 67 ιδιωτικό static void computeAndSleepLoop (int sleepInterval) {68 int MAX_CALLS = 10000; 69 ενώ (callCount <MAX_CALLS) {70 computeAndSleep (sleepInterval); 71} 72} 73 74 ιδιωτικό static void computeAndSleep (int sleepInterval) {75 int COMPUTATIONS = 1000; 76 διπλό αποτέλεσμα; 77 78 // Υπολογισμός. 79 callCount ++; 80 για (int i = 0; i <COMPUTATIONS; i ++) {81 result = Math.atan (callCount * Math.random ()); 82} 83 84 // ύπνος. 85 δοκιμάστε το {86 Thread.currentThread (). Sleep (sleepInterval); 87} catch (InterruptException δηλαδή) {88 // Μην κάνετε τίποτα. 89} 90 91} // Τέλος υπολογισμόςAndSleep. 92} // Τέλος BusyWork. 

Υπάρχουν τρία νήματα - Εργασιομανής, Εργαστής, και Αδέξιος - των οποίων η ηθική εργασίας ποικίλλει ανάλογα με τις τάξεις μεγέθους, ανάλογα με την εργασία που επιλέγουν να κάνουν. Εξετάστε την ενότητα Δείγματα CPU του προφίλ που φαίνεται παρακάτω. Τα τρία ίχνη με την υψηλότερη βαθμολογία δείχνουν ότι η CPU πέρασε τον περισσότερο χρόνο της υπολογίζοντας τυχαίους αριθμούς και εφαπτομενικά τόξα, όπως θα περιμέναμε:

CPU SAMPLES BEGIN (total = 935) Tue 4 Sep 20:44:49 2001 κατάταξη αυτοαθροίσματος μέθοδος ανίχνευσης 1 39,04% 39,04% 365 2040 java / util / Random.next 2 26,84% 65,88% 251 2042 java / util / Random. nextDouble 3 10.91% 76.79% 102 2041 java / lang / StrictMath.atan 4 8.13% 84.92% 76 2046 BusyWork.computeAndSleep 5 4.28% 89.20% 40 2050 java / lang / Math.atan 6 3.21% 92.41% 30 2045 java / lang / Math.random 7 2.25% 94.65% 21 2051 java / lang / Math.random 8 1.82% 96.47% 17 2044 java / util / Random.next 9 1.50% 97.97% 14 2043 java / util / Random.nextDouble 10 0.43% 98.40% 4 2047 BusyWork.computeAndSleep 11 0.21% 98.61% 2 2048 java / lang / StrictMath.atan 12 0.11% 98.72% 1 1578 java / io / BufferedReader.readLine 13 0.11% 98.82% 1 2054 java / lang / Thread.sleep 14 0.11% 98.93% 1 1956 java / security / PermissionCollection.setReadOnly 15 0.11% 99.04% 1 2055 java / lang / Thread.sleep 16 0.11% 99.14% 1 1593 java / lang / String.valueOf 17 0.11% 99.25% 1 2052 java / lang / Math.random 18 0.11% 99.36% 1 2049 java / util / Random.nextDouble 19 0.11% 99.47% 1 2031 BusyWork.computeAndSleep 20 0,11% 99,57% 1 1530 sun / io / CharToByteISO8859_1.μετατροπή ... 

Σημειώστε ότι καλεί στο BusyWork.computeAndSleep () η μέθοδος καταλαμβάνει 8,13 τοις εκατό, 0,43 τοις εκατό και 0,11 τοις εκατό για το Εργασιομανής, Εργαστής, και Αδέξιος νήματα, αντίστοιχα. Μπορούμε να πούμε ποια νήματα είναι αυτά εξετάζοντας τα ίχνη που αναφέρονται στην στήλη ιχνών της ενότητας CPU Samples παραπάνω (κατατάξεις 4, 10 και 19) στην ακόλουθη ενότητα Trace: