Diagnosis masalah runtime umum dengan hprof

Kebocoran memori dan kebuntuan dan CPU hogs, oh my! Pengembang aplikasi Java sering menghadapi masalah runtime ini. Mereka bisa sangat menakutkan dalam aplikasi yang kompleks dengan banyak utas yang berjalan melalui ratusan ribu baris kode - aplikasi yang tidak dapat Anda kirimkan karena tumbuh dalam memori, menjadi tidak aktif, atau menelan lebih banyak siklus CPU daripada yang seharusnya.

Bukan rahasia lagi bahwa alat profil Java memiliki cara yang panjang untuk mengejar rekan-rekan bahasa alternatif mereka. Banyak alat canggih sekarang tersedia untuk membantu kami melacak penyebab di balik masalah umum tersebut. Tetapi bagaimana Anda mengembangkan kepercayaan diri pada kemampuan Anda untuk menggunakan alat-alat ini secara efektif? Bagaimanapun, Anda menggunakan alat untuk mendiagnosis perilaku kompleks yang tidak Anda pahami. Untuk menambah penderitaan Anda, data yang diberikan oleh alat ini cukup rumit dan informasi yang Anda cari atau cari tidak selalu jelas.

Ketika dihadapkan dengan masalah serupa dalam inkarnasi saya sebelumnya sebagai fisikawan eksperimental, saya membuat eksperimen kontrol dengan hasil yang dapat diprediksi. Ini membantu saya mendapatkan kepercayaan pada sistem pengukuran yang saya gunakan dalam eksperimen yang menghasilkan hasil yang kurang dapat diprediksi. Demikian pula, artikel ini menggunakan alat profil hprof untuk memeriksa tiga aplikasi kontrol sederhana yang menunjukkan tiga perilaku masalah umum yang tercantum di atas. Meskipun tidak semudah beberapa alat komersial di pasaran, hprof disertakan dengan Java 2 JDK dan, seperti yang akan saya tunjukkan, dapat secara efektif mendiagnosis perilaku ini.

Jalankan dengan hprof

Menjalankan program Anda dengan hprof itu mudah. Cukup aktifkan runtime Java dengan opsi baris perintah berikut, seperti yang dijelaskan dalam dokumentasi alat JDK untuk peluncur aplikasi Java:

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

Daftar suboption tersedia dengan [:help]opsi yang ditampilkan. Saya membuat contoh dalam artikel ini menggunakan port Blackdown dari JDK 1.3-RC1 untuk Linux dengan perintah peluncuran berikut:

java -classic -Xrunhprof: heap = situs, cpu = sampel, kedalaman = 10, monitor = y, thread = y, doe = y MemoryLeak 

Daftar berikut menjelaskan fungsi dari setiap suboption yang digunakan pada perintah sebelumnya:

  • heap=sites: Memberi tahu hprof untuk membuat jejak tumpukan yang menunjukkan di mana memori dialokasikan
  • cpu=samples: Memberi tahu hprof untuk menggunakan sampling statistik untuk menentukan di mana CPU menghabiskan waktunya
  • depth=10: Memberi tahu hprof untuk menampilkan jejak tumpukan sedalam 10 level, paling banyak
  • monitor=y: Memberi tahu hprof untuk menghasilkan informasi pada monitor pertentangan yang digunakan untuk menyinkronkan pekerjaan beberapa utas
  • thread=y: Memberi tahu hprof untuk mengidentifikasi utas dalam pelacakan tumpukan
  • doe=y: Memberi tahu hprof untuk membuat dump data profil saat keluar

Jika Anda menggunakan JDK 1.3, Anda perlu mematikan kompiler HotSpot default dengan -classicopsi. HotSpot memiliki profilernya sendiri, dipanggil melalui -Xprofopsi, yang menggunakan format keluaran berbeda dari yang akan saya jelaskan di sini.

Menjalankan program Anda dengan hprof akan meninggalkan sebuah file bernama java.hprof.txtdi direktori kerja Anda; file ini berisi informasi profil yang dikumpulkan saat program Anda berjalan. Anda juga dapat membuat dump kapan saja saat program Anda berjalan dengan menekan Ctrl- \ di jendela konsol Java Anda di Unix atau Ctrl-Break di Windows.

Anatomi file keluaran hprof

File keluaran hprof berisi bagian yang menjelaskan berbagai karakteristik program Java yang diprofilkan. Ini dimulai dengan tajuk yang menjelaskan formatnya, yang diklaim tajuk dapat berubah tanpa pemberitahuan.

Bagian Thread dan Trace file output membantu Anda mengetahui thread apa yang aktif ketika program Anda berjalan dan apa fungsinya. Bagian Thread menyediakan daftar semua thread yang dimulai dan diakhiri selama masa pakai program. Bagian Jejak menyertakan daftar jejak tumpukan bernomor untuk beberapa utas. Nomor pelacakan tumpukan ini direferensikan silang di bagian file lainnya.

Bagian Heap Dump dan Sites membantu Anda menganalisis penggunaan memori. Bergantung pada heapsuboption yang Anda pilih saat memulai mesin virtual (VM), Anda bisa mendapatkan dump dari semua objek langsung di Java heap ( heap=dump) dan / atau daftar situs alokasi yang diurutkan yang mengidentifikasi objek yang paling banyak dialokasikan ( heap=sites).

Bagian Sampel CPU dan Waktu CPU membantu Anda memahami penggunaan CPU; bagian yang Anda dapatkan bergantung pada cpusuboption ( cpu=samplesatau cpu=time) Anda. Sampel CPU menyediakan profil eksekusi statistik. Waktu CPU mencakup pengukuran berapa kali metode tertentu dipanggil dan berapa lama setiap metode untuk dieksekusi.

Bagian Monitor Time dan Monitor Dump membantu Anda memahami bagaimana sinkronisasi memengaruhi kinerja program Anda. Monitor Time menunjukkan berapa banyak waktu thread Anda mengalami pertikaian untuk sumber daya yang terkunci. Monitor Dump adalah snapshot dari monitor yang sedang digunakan. Seperti yang akan Anda lihat, Monitor Dump berguna untuk menemukan kebuntuan.

Diagnosis kebocoran memori

Di Java, saya mendefinisikan kebocoran memori sebagai (biasanya) kegagalan yang tidak disengaja untuk mendereferensi objek yang dibuang sehingga pengumpul sampah tidak dapat memperoleh kembali memori yang mereka gunakan. The MemoryLeakprogram Listing 1 sederhana:

Kode 1. Program MemoryLeak

01 import java.util.Vector; 02 03 kelas publik 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 = new ConsumerContainer (); 11 12 sementara (objectHolder.size () <MAX_CONSUMERS) {13 System.out.println ("Allocating object" + 14 Integer.toString (objectHolder.size ()) 15); 16 objectHolder.add (new MemoryConsumer ()); 17 coba {18 Thread.currentThread (). Sleep (SLEEP_BETWEEN_ALLOCS); 19} catch (InterruptedException yaitu) {20 // Tidak melakukan apa-apa. 21} 22} // sementara. 23} // utama. 24 25} // Akhir dari MemoryLeak. 26 27 / ** Kelas kontainer bernama untuk menampung referensi objek. * / 28 class ConsumerContainer extends Vector {} 29 30 / ** Class yang menggunakan jumlah memori tetap. * / 31 kelas MemoryConsumer {32 public int static final MEMORY_BLOCK = 1024;33 byte publik [] memoryHoldingArray; 34 35 MemoryConsumer () {36 memoryHoldingArray = byte baru [MEMORY_BLOCK]; 37} 38} // Akhiri MemoryConsumer.

Ketika program berjalan, itu membuat sebuah ConsumerContainerobjek, kemudian mulai membuat dan menambahkan MemoryConsumerobjek setidaknya berukuran 1 KB ke ConsumerContainerobjek itu. Menjaga agar objek tetap dapat diakses membuatnya tidak tersedia untuk pengumpulan sampah, yang menyimulasikan kebocoran memori.

Mari kita lihat bagian tertentu dari file profil. Beberapa baris pertama di bagian Situs dengan jelas menunjukkan apa yang terjadi:

SITUS DIMULAI (diurutkan berdasarkan byte langsung) Sen 3 Sep 19:16:29 2001 persen peringkat kelas tumpukan yang dialokasikan langsung sendiri akumulasi byte keberatan byte keberatan nama jejak 1 97,31% 97,31% 10280000 10000 10280000 10000 1995 [B 2 0,39% 97,69% 40964 1 81880 10 1996 [L; 3 0.38% 98.07% 40000 10000 40000 10000 1994 MemoryConsumer 4 0.16% 98.23% 16388 1 16388 1 1295 [C 5 0.16% 98.38% 16388 1 16388 1 1304 [C ...

Ada 10.000 objek tipe byte[]( [Bdalam VM-speak) serta 10.000 MemoryConsumerobjek. Array byte mengambil 10.280.000 byte, jadi tampaknya ada overhead tepat di atas byte mentah yang digunakan setiap array. Karena jumlah objek yang dialokasikan sama dengan jumlah objek aktif, kita dapat menyimpulkan bahwa tidak ada objek ini yang bisa dikumpulkan sampahnya. Ini sesuai dengan harapan kami.

Hal menarik lainnya: memori yang dilaporkan dikonsumsi oleh MemoryConsumerobjek tidak termasuk memori yang dikonsumsi oleh array byte. Ini menunjukkan bahwa alat pembuatan profil kami tidak mengekspos hubungan penahanan hierarki, melainkan statistik kelas demi kelas. Hal ini penting untuk dipahami saat menggunakan hprof untuk menunjukkan kebocoran memori.

Sekarang, dari mana asal array byte yang bocor itu? Perhatikan bahwa MemoryConsumerobjek dan referensi array byte jejak 1994dan 1995di bagian Jejak berikut. Lihatlah, jejak ini memberitahu kita bahwa MemoryConsumerobjek dibuat dalam metode MemoryLeakkelas main()dan array byte dibuat di konstruktor ( ()metode dalam VM-speak). Kami telah menemukan kebocoran memori, nomor baris, dan semuanya:

TRACE 1994: (thread = 1) MemoryLeak.main (MemoryLeak.java:16) TRACE 1995: (thread = 1) MemoryConsumer. (MemoryLeak.java:36) MemoryLeak.main (MemoryLeak.java:16) 

Diagnosis babi CPU

Dalam Listing 2, sebuah BusyWorkkelas memiliki setiap utas yang memanggil metode yang mengatur seberapa banyak utas bekerja dengan memvariasikan waktu tidurnya di antara pertarungan melakukan penghitungan intensif CPU:

Listing 2. CPUHog program

01 /** Main class for control test. */ 02 public class CPUHog { 03 public static void main(String[] args) { 04 05 Thread slouch, workingStiff, workaholic; 06 slouch = new Slouch(); 07 workingStiff = new WorkingStiff(); 08 workaholic = new Workaholic(); 09 10 slouch.start(); 11 workingStiff.start(); 12 workaholic.start(); 13 } 14 } 15 16 /** Low CPU utilization thread. */ 17 class Slouch extends Thread { 18 public Slouch() { 19 super("Slouch"); 20 } 21 public void run() { 22 BusyWork.slouch(); 23 } 24 } 25 26 /** Medium CPU utilization thread. */ 27 class WorkingStiff extends Thread { 28 public WorkingStiff() { 29 super("WorkingStiff"); 30 } 31 public void run() { 32 BusyWork.workNormally(); 33 } 34 } 35 36 /** High CPU utilization thread. */ 37 class Workaholic extends Thread { 38 public Workaholic() { 39 super("Workaholic"); 40 } 41 public void run() { 42 BusyWork.workTillYouDrop(); 43 } 44 } 45 46 /** Class with static methods to consume varying amounts 47 * of CPU time. */ 48 class BusyWork { 49 50 public static int callCount = 0; 51 52 public static void slouch() { 53 int SLEEP_INTERVAL = 1000; 54 computeAndSleepLoop(SLEEP_INTERVAL); 55 } 56 57 public static void workNormally() { 58 int SLEEP_INTERVAL = 100; 59 computeAndSleepLoop(SLEEP_INTERVAL); 60 } 61 62 public static void workTillYouDrop() { 63 int SLEEP_INTERVAL = 10; 64 computeAndSleepLoop(SLEEP_INTERVAL); 65 } 66 67 private static void computeAndSleepLoop(int sleepInterval) { 68 int MAX_CALLS = 10000; 69 while (callCount < MAX_CALLS) { 70 computeAndSleep(sleepInterval); 71 } 72 } 73 74 private static void computeAndSleep(int sleepInterval) { 75 int COMPUTATIONS = 1000; 76 double result; 77 78 // Compute. 79 callCount++; 80 for (int i = 0; i < COMPUTATIONS; i++) { 81 result = Math.atan(callCount * Math.random()); 82 } 83 84 // Sleep. 85 try { 86 Thread.currentThread().sleep(sleepInterval); 87 } catch (InterruptedException ie) { 88 // Do nothing. 89 } 90 91 } // End computeAndSleep. 92 } // End BusyWork. 

There are three threads -- Workaholic, WorkingStiff, and Slouch -- whose work ethics vary by orders of magnitude, judging by the work they choose to do. Examine the profile's CPU Samples section shown below. The three highest ranked traces show that the CPU spent most of its time calculating random numbers and arc tangents, as we would expect:

CPU SAMPLES BEGIN (total = 935) Tue Sep 4 20:44:49 2001 rank self accum count trace method 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.convert ... 

Perhatikan bahwa panggilan ke BusyWork.computeAndSleep()metode mengambil 8.13 persen, 0,43 persen, dan 0,11 persen untuk Workaholic, WorkingStiffdan Slouchbenang, masing-masing. Kita dapat mengetahui utas mana ini dengan memeriksa jejak yang dirujuk di kolom jejak bagian CPU Samples di atas (peringkat 4, 10, dan 19) di bagian Jejak berikut: