Saat aplikasi produksi berperilaku aneh — CPU melonjak, thread macet, memori bengkak — tiga alat kecil di folder bin JDK menjadi tim forensik pertama yang masuk: jcmd mengirim perintah langsung ke JVM, jstack membekukan daftar seluruh thread beserta posisi kodenya, dan jmap menyensus objek di heap. Ketiganya bekerja lewat mekanisme attach tanpa mengganggu aplikasi, dan semuanya mengambil PID dari jps sebagaimana dibahas Chapter Tools Monitoring JVM. Pada chapter ini kita jalankan ketiganya terhadap program sasaran yang sama sehingga keluarannya dapat dibaca berdampingan, lalu mengajak salah satu program sengaja macet untuk melihat kemampuan deteksi deadlock milik jstack.
jcmd Pisau Serbaguna #
jcmd adalah gerbang perintah ke JVM hidup: jcmd <pid> <perintah> meminta JVM menjalankan satu dari puluhan perintah bawaan, dan jcmd <pid> help mendaftar semuanya. Dua perintah yang paling sering dipakai di penyelidikan memori adalah VM.version untuk memastikan mesin apa yang diamati, dan GC.heap_info untuk memotret kondisi heap G1 saat itu juga:
jcmd 24968 VM.version
jcmd 24968 GC.heap_info24968:
OpenJDK 64-Bit Server VM version 25.0.4.1+1-LTS
JDK 25.0.4.1
24968:
garbage-first heap total reserved 1048576K, committed 67584K, used 25313K [0x00000000c0000000, 0x0000000100000000)
region size 1024K, 23 young (23552K), 2 survivors (2048K)Baris garbage-first heap mengonfirmasi collector G1 aktif; reserved adalah ruang yang dijanjikan ke heap (1 GB pada mesin uji — ergonomis, berbeda di mesin lain), committed ruang yang sudah benar-benar dipetakan, dan used yang terisi data saat sampling. Baris kedua merinci wilayahnya dalam satuan region G1: 23 region muda dan 2 region survivor. Selain kedua perintah ini, VM.flags mencetak seluruh bendera aktif — versi hidup dari PrintFlagsFinal yang kita bahas di Chapter JVM Options — dan Thread.print mencetak thread dump dari dalam jcmd itu sendiri.
jstack untuk Thread Dump #
jstack <pid> membekukan seluruh thread dalam satu potret: nama thread, status, dan stack trace persis di baris kode mana thread berada. Pada program sasaran kita — yang sedang tertidur di tengah fase diam — potret thread main-nya berbunyi:
jstack 24968"main" #3 [24970] prio=5 os_prio=0 cpu=417.52ms elapsed=1.87s tid=0x00007f13f002af60 nid=24970 waiting on condition [0x00007f13f6f1a000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleepNanos0([email protected]/Native Method)
at java.lang.Thread.sleepNanos([email protected]/Thread.java:509)
at java.lang.Thread.sleep([email protected]/Thread.java:540)
at TargetDiagnosis.main(TargetDiagnosis.java:14)Cara membacanya: baris java.lang.Thread.State: TIMED_WAITING (sleeping) menyatakan thread menunggu dengan batas waktu, dan stack teratas menunjuk TargetDiagnosis.main baris 14 — tepat baris Thread.sleep(4_000) di program sasaran kita. Status-status penting lainnya: RUNNABLE berarti thread sedang mengeksekusi kode; BLOCKED menunggu masuk wilayah synchronized yang dikuasai thread lain; WAITING menunggu tanpa batas seperti di Object.wait(). Dump diambil saat thread macet biasanya menunjuk baris yang sama berulang pada dump beruntun — itulah cara klasik menemukan titik kemacetan.
Kemampuan paling terkenal jstack adalah deteksi deadlock otomatis. Program kecil berikut sengaja menciptakan dua thread yang saling mengunci:
static final Object kunciA = new Object();
static final Object kunciB = new Object();
Thread utara = new Thread(() -> {
synchronized (kunciA) {
sleepSebentar();
synchronized (kunciB) { } // menunggu kunciB yang dipegang selatan
}
}, "utara");
Thread selatan = new Thread(() -> {
synchronized (kunciB) {
sleepSebentar();
synchronized (kunciA) { } // menunggu kunciA yang dipegang utara
}
}, "selatan");Saat jstack ditempelkan ke proses yang macet itu, alat tidak hanya mencetak dump — ia menganalisis grafik kunci dan menyimpulkan kebuntuan:
Found one Java-level deadlock:
=============================
"utara":
waiting to lock monitor 0x00007f926800a540 (object 0x00000000c36fb2f8, a java.lang.Object),
which is held by "selatan"
"selatan":
waiting to lock monitor 0x00007f9264001450 (object 0x00000000c36fb2e8, a java.lang.Object),
which is held by "utara"Ringkasan itu adalah diagnosis lengkap dalam empat baris: thread utara menunggu kunci yang dipegang selatan, dan sebaliknya — lingkaran yang tidak akan pernah terurai sendiri. Tanpa temuan otomatis seperti ini, kita harus membaca ratusan baris dump secara manual menyusuri monitor yang diperebutkan.
jmap untuk Sensus Objek #
jmap -histo <pid> menghitung seluruh objek di heap per jenis class, terurut dari yang paling banyak memakan byte — sensus kilat tanpa menulis berkas apa pun:
jmap -histo 24968 num #instances #bytes class name (module)
-------------------------------------------------------
1: 176219 12385016 [B ([email protected])
2: 101167 2428008 java.lang.String ([email protected])
3: 8559 1838152 [I ([email protected])
4: 170 712496 [Ljdk.internal.vm.FillerElement; ([email protected])Nama class di sini memakai notasi array JVM: [B adalah array byte, [I array int, [Ljava.lang.Object; array referensi. Pada sasaran kita, [B memimpin — wajar, karena program menahan puluhan blok byte[] — dan kelas internal seperti FillerElement adalah artefak internal G1 untuk menandai ruang kosong, bukan objek aplikasi. Histogram semacam ini adalah langkah pertama diagnosis memory leak dari Chapter sebelumnya: jenis class yang jumlahnya tumbuh terus antar dua sensus adalah tersangka utamanya.
Saat bukti lebih dalam dibutuhkan, jmap -dump:format=b,file=heap.hprof <pid> membekukan seluruh heap ke berkas .hprof — bahan baku untuk Eclipse MAT dan JDK Mission Control yang membaca rantai referensi penahan objek. Berbeda dengan -histo yang nyaris gratis, dump penuh menghentikan aplikasi beberapa detik dan menghasilkan berkas sebesar heap; di produksi, kombinasikan dengan -XX:+HeapDumpOnOutOfMemoryError agar dump diambil otomatis tepat saat kehabisan memori, bukan diambil terburu-buru setelahnya.
Target Latihan #
Semua keluaran di atas diambil dari satu program sasaran yang sengaja dirancang mudah diamati — tiga fase dengan jeda di tengahnya:
import java.util.ArrayList;
import java.util.List;
public class TargetDiagnosis {
public static void main(String[] args) throws Exception {
List<byte[]> muatan = new ArrayList<>();
System.out.println("fase 1: pemanasan");
for (int i = 0; i < 50; i++) {
muatan.add(new byte[64 * 1024]); // 64 KB per objek
}
System.out.println("fase 2: diam 4 detik (saat menempel alat di PID ini)");
Thread.sleep(4_000);
System.out.println("fase 3: gelombang alokasi");
for (int i = 0; i < 20; i++) {
byte[] sementara = new byte[1024 * 1024]; // 1 MB, langsung jadi sampah
sementara[0] = 1;
Thread.sleep(100);
}
System.out.println("selesai, total muatan tertahan: " + muatan.size() + " blok");
}
}Output yang Dihasilkan:
fase 1: pemanasan
fase 2: diam 4 detik (saat menempel alat di PID ini)
fase 3: gelombang alokasi
selesai, total muatan tertahan: 50 blokAnatomi sasarannya sengaja menggemakan materi-materi sebelumnya:
- Fase 1 (
muatan.add(new byte[64 * 1024])): 50 objek yang ditahan — penyintas yang menempati heap dan terlihat dijmap -histosebagai[B. - Fase 2 (
Thread.sleep(4_000)): jeda empat detik adalah jendela operasi — jalankan program, ambil PID darijps -l, lalu tempelkanjcmd,jstack, danjmapselama program diam. Stack dump akan menunjuk baris 14 ini. - Fase 3 (
new byte[1024 * 1024]tanpa disimpan): objek dibuat lalu ditinggalkan tiap 100 ms — sampah muda yang menghidupkan minor GC dan menggerakkan kolomEpadajstat, sebagaimana dibahas Chapter Object Lifecycle.
Dengan tiga alat ini kita bisa menjawab “di mana thread berada” dan “apa yang mengisi heap” pada JVM hidup. Satu bab tersisa di Section ini: menyatukan seluruh pengetahuan — pengukuran, collector, ukuran heap, generasi — menjadi alur kerja memory tuning yang disiplin dan tidak mengandalkan tebakan.
Referensi #
- Oracle: jcmd Tool Reference (JDK 25)
- Oracle: jstack Tool Reference (JDK 25)
- Oracle: jmap Tool Reference (JDK 25)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
TargetDiagnosis.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class TargetDiagnosis {
public static void main(String[] args) throws Exception {
List<byte[]> muatan = new ArrayList<>();
System.out.println("fase 1: pemanasan");
for (int i = 0; i < 50; i++) {
muatan.add(new byte[64 * 1024]); // 64 KB per objek
}
System.out.println("fase 2: diam 4 detik (saat menempel alat di PID ini)");
Thread.sleep(4_000);
System.out.println("fase 3: gelombang alokasi");
for (int i = 0; i < 20; i++) {
byte[] sementara = new byte[1024 * 1024]; // 1 MB, langsung jadi sampah
sementara[0] = 1;
Thread.sleep(100);
}
System.out.println("selesai, total muatan tertahan: " + muatan.size() + " blok");
}
}