Kode yang lolos seluruh pengujian tetap bisa berperilaku aneh di produksi: beban nyata, data nyata, dan interaksi antar komponen yang tidak pernah terjadi di mesin pengembang. Insiden produksi berbeda dari debugging lokal: debugger tidak selalu bisa ditempelkan ke proses yang sibuk melayani pengguna, dan sebagian bukti berumur pendek — isi heap dan status thread hilang begitu proses direstart. Chapter ini menyusun metode diagnosis sistematis: urutan penanganan insiden, alat JDK pada proses hidup, dan tabel keputusan gejala → bukti → tindakan.
Urutan Penanganan Insiden #
Naluri pertama saat insiden adalah me-restart agar layanan pulih cepat — tetapi restart juga menghapus bukti: heap yang hampir penuh, thread yang macet, dan antrean yang menumpuk tidak bisa diperiksa lagi setelah proses baru menyala. Urutan yang benar membaliknya: amankan bukti lebih dulu (thread dump, heap dump, rekaman JFR), barulah pulihkan layanan, lalu cari akar masalah dari bukti yang tersimpan.
Tabel Keputusan Diagnosis #
Insiden datang sebagai gejala; gejala yang sama bisa berakar pada masalah berbeda. Tabel keputusan menuntun langkah pertama — pilih bukti yang dikumpulkan sebelum melompat ke kesimpulan:
import java.util.List;
/** Tabel keputusan diagnosis produksi: gejala -> sumber bukti -> tindakan pertama. */
public class PetaDiagnosis {
/** Satu baris tabel keputusan: gejala, indikator pendukung, bukti yang dikumpulkan, tindakan. */
record Baris(String gejala, String indikator, String bukti, String tindakan) {}
/** Tabel keputusan insiden — tiap baris adalah satu jalur diagnosis. */
static final List<Baris> TABEL = List.of(
new Baris("latensi p95 menanjak", "CPU rendah",
"thread dump (jcmd Thread.print)", "cari thread WAITING/BLOCKED dan pemilik kunci"),
new Baris("latensi p95 menanjak", "CPU jenuh",
"rekaman JFR (jfr print)", "perbaiki hotspot teratas dari profil"),
new Baris("OutOfMemoryError heap", "heap terus membesar",
"heap dump (jcmd GC.heap_dump)", "analisis retensi objek terbesar"),
new Baris("container mati mendadak", "exit code 137",
"anggaran memori non-heap", "perbesar limit atau turunkan MaxRAMPercentage"),
new Baris("aplikasi tidak menyala", "berhenti saat startup",
"log startup + tabel konfigurasi", "periksa environment variable wajib"),
new Baris("koneksi database habis", "antrean pool menanjak",
"metrik pool + thread dump", "cari koneksi yang diambil tanpa ditutup"));
/** Jalankan satu insiden lewat tabel: temukan baris yang cocok, cetak jalurnya. */
static void diagnosis(int nomor, String gejala, String indikator) {
System.out.println("Insiden " + nomor + ": " + gejala + " (" + indikator + ")");
Baris cocok = TABEL.stream()
.filter(b -> b.gejala().equals(gejala) && b.indikator().equals(indikator))
.findFirst()
.orElseThrow();
System.out.println(" bukti : kumpulkan " + cocok.bukti());
System.out.println(" tindakan: " + cocok.tindakan());
System.out.println();
}
public static void main(String[] args) {
System.out.println("== Tabel keputusan diagnosis ==");
System.out.printf("%-24s %-22s %-32s %s%n",
"gejala", "indikator", "bukti yang dikumpulkan", "tindakan pertama");
for (Baris b : TABEL) {
System.out.printf("%-24s %-22s %-32s %s%n",
b.gejala(), b.indikator(), b.bukti(), b.tindakan());
}
System.out.println();
System.out.println("== Tiga insiden melewati tabel ==");
diagnosis(1, "latensi p95 menanjak", "CPU rendah");
diagnosis(2, "container mati mendadak", "exit code 137");
diagnosis(3, "koneksi database habis", "antrean pool menanjak");
System.out.println("== Urutan penanganan insiden ==");
String[] langkah = {
"1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun",
"2. pulihkan layanan: rollback atau scale-out dengan data yang ada",
"3. cari akar masalah lewat tabel keputusan di atas",
"4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannya"
};
for (String l : langkah) {
System.out.println(" " + l);
}
}
}Output yang Dihasilkan:
== Tabel keputusan diagnosis ==
gejala indikator bukti yang dikumpulkan tindakan pertama
latensi p95 menanjak CPU rendah thread dump (jcmd Thread.print) cari thread WAITING/BLOCKED dan pemilik kunci
latensi p95 menanjak CPU jenuh rekaman JFR (jfr print) perbaiki hotspot teratas dari profil
OutOfMemoryError heap heap terus membesar heap dump (jcmd GC.heap_dump) analisis retensi objek terbesar
container mati mendadak exit code 137 anggaran memori non-heap perbesar limit atau turunkan MaxRAMPercentage
aplikasi tidak menyala berhenti saat startup log startup + tabel konfigurasi periksa environment variable wajib
koneksi database habis antrean pool menanjak metrik pool + thread dump cari koneksi yang diambil tanpa ditutup
== Tiga insiden melewati tabel ==
Insiden 1: latensi p95 menanjak (CPU rendah)
bukti : kumpulkan thread dump (jcmd Thread.print)
tindakan: cari thread WAITING/BLOCKED dan pemilik kunci
Insiden 2: container mati mendadak (exit code 137)
bukti : kumpulkan anggaran memori non-heap
tindakan: perbesar limit atau turunkan MaxRAMPercentage
Insiden 3: koneksi database habis (antrean pool menanjak)
bukti : kumpulkan metrik pool + thread dump
tindakan: cari koneksi yang diambil tanpa ditutup
== Urutan penanganan insiden ==
1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun
2. pulihkan layanan: rollback atau scale-out dengan data yang ada
3. cari akar masalah lewat tabel keputusan di atas
4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannyaMari kita bedah struktur tabelnya:
record Baris(gejala, indikator, bukti, tindakan): satu baris tabel keputusan sebagai data — gejala dan indikator kunci pencarian, bukti dan tindakan hasilnya. Tabel sebagai data (bukanif-elseberantai) bisa direview dan ditambah tanpa menyentuh logika.- Dua baris “latensi p95 menanjak”: gejala sama, indikator berbeda, bukti berbeda — CPU rendah berarti thread menunggu, CPU jenuh berarti komputasi. Indikator memangkas ruang dugaan;
diagnosis(...)mencari baris dengan pasangan kunci yang cocok. - Baris exit code 137: akar masalahnya di luar JVM — kernel menghentikan container karena batas cgroup terlampaui (anggaran non-heap di chapter Containerizing Aplikasi Java). Memeriksa heap tidak menolong bila yang habis justru komponen non-heap.
- Urutan empat langkah: bukti → pulih → akar masalah → postmortem. Postmortem blameless mencatat kejadian beserta pencegahannya, bukan siapa yang keliru — tim yang takut disalahkan menyembunyikan kejadian berikutnya.
Alat Diagnosis pada Proses Hidup #
jcmd adalah pintu utama: satu alat yang mengirim perintah diagnosis ke proses Java mana pun di mesin yang sama, tanpa agent tambahan. Tanpa argumen (atau -l), jcmd mencantumkan proses Java yang berjalan beserta PID-nya. Sesi berikut menargetkan ServerTidur:
jcmd -lOutput yang Dihasilkan:
8837 jdk.jcmd/sun.tools.jcmd.JCmd -l
8814 ServerTidurBaris kedua adalah target kita — PID 8814. Saat insiden latensi, perintah yang paling sering dipakai adalah Thread.print; potongan berikut menampilkan blok thread utama:
jcmd 8814 Thread.printOutput yang Dihasilkan (potongan thread main):
8814:
2026-10-16 03:09:22
"main" #3 [8815] prio=5 os_prio=0 cpu=21.79ms elapsed=2.38s tid=0x00007f488002acb0 nid=8815 waiting on condition [0x00007f4885302000]
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)PID, tid/nid, cpu, dan elapsed berbeda tiap proses — yang stabil dan diagnostik adalah nama thread, Thread.State, dan isi stack. Thread main di contoh berstatus TIMED_WAITING (sleeping) karena memang tidur; pada aplikasi sakit, cari thread WAITING/BLOCKED yang lama lalu baca stack-nya. Alat pendampingnya:
| Perintah | Bukti yang dihasilkan | Pertanyaan yang dijawab |
|---|---|---|
jcmd <pid> Thread.print | thread dump seluruh thread | thread apa yang menunggu atau macet? |
jcmd <pid> GC.heap_info | ringkasan heap | seberapa penuh heap? |
jcmd <pid> GC.heap_dump | berkas .hprof snapshot heap | objek apa yang memakan memori? |
jcmd <pid> VM.flags | seluruh flag JVM aktif | konfigurasi runtime mana yang berlaku? |
jfr print / jfr summary | event dari rekaman .jfr | di mana waktu prosesor dibelanjakan? |
Jebakan Umum #
Tiga kekeliruan klasik: menyamakan OOMKill dengan OutOfMemoryError (exit code 137 berarti kernel menghentikan container karena batas cgroup — heap JVM bisa jadi masih sehat; yang bocor justru metaspace, thread stack, atau direct buffer); klaim “berhasil di laptop, gagal di produksi” yang biasanya selesai pada tabel konfigurasi startup (chapter Externalized Configuration) — bandingkan baris demi baris; dan PID yang diperiksa ternyata sisa pod lama, bukan proses bermasalah.
Diagnosis yang tertata memberi akar masalah — dan perbaikannya harus menyeberang ke produksi dengan aman. Chapter penutup section ini mengatur penyeberangan itu: versi rilis yang berdisiplin, changelog yang bisa diaudit, dan strategi rollout yang selalu punya jalur mundur.
Referensi #
- Oracle: Troubleshooting Guide JDK 25
- Oracle: jcmd Reference untuk JDK 25
- Oracle: jfr Reference untuk JDK 25
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PetaDiagnosis.java
Unduh berkasimport java.util.List;
/** Tabel keputusan diagnosis produksi: gejala -> sumber bukti -> tindakan pertama. */
public class PetaDiagnosis {
/** Satu baris tabel keputusan: gejala, indikator pendukung, bukti yang dikumpulkan, tindakan. */
record Baris(String gejala, String indikator, String bukti, String tindakan) {}
/** Tabel keputusan insiden — tiap baris adalah satu jalur diagnosis. */
static final List<Baris> TABEL = List.of(
new Baris("latensi p95 menanjak", "CPU rendah",
"thread dump (jcmd Thread.print)", "cari thread WAITING/BLOCKED dan pemilik kunci"),
new Baris("latensi p95 menanjak", "CPU jenuh",
"rekaman JFR (jfr print)", "perbaiki hotspot teratas dari profil"),
new Baris("OutOfMemoryError heap", "heap terus membesar",
"heap dump (jcmd GC.heap_dump)", "analisis retensi objek terbesar"),
new Baris("container mati mendadak", "exit code 137",
"anggaran memori non-heap", "perbesar limit atau turunkan MaxRAMPercentage"),
new Baris("aplikasi tidak menyala", "berhenti saat startup",
"log startup + tabel konfigurasi", "periksa environment variable wajib"),
new Baris("koneksi database habis", "antrean pool menanjak",
"metrik pool + thread dump", "cari koneksi yang diambil tanpa ditutup"));
/** Jalankan satu insiden lewat tabel: temukan baris yang cocok, cetak jalurnya. */
static void diagnosis(int nomor, String gejala, String indikator) {
System.out.println("Insiden " + nomor + ": " + gejala + " (" + indikator + ")");
Baris cocok = TABEL.stream()
.filter(b -> b.gejala().equals(gejala) && b.indikator().equals(indikator))
.findFirst()
.orElseThrow();
System.out.println(" bukti : kumpulkan " + cocok.bukti());
System.out.println(" tindakan: " + cocok.tindakan());
System.out.println();
}
public static void main(String[] args) {
System.out.println("== Tabel keputusan diagnosis ==");
System.out.printf("%-24s %-22s %-32s %s%n",
"gejala", "indikator", "bukti yang dikumpulkan", "tindakan pertama");
for (Baris b : TABEL) {
System.out.printf("%-24s %-22s %-32s %s%n",
b.gejala(), b.indikator(), b.bukti(), b.tindakan());
}
System.out.println();
System.out.println("== Tiga insiden melewati tabel ==");
diagnosis(1, "latensi p95 menanjak", "CPU rendah");
diagnosis(2, "container mati mendadak", "exit code 137");
diagnosis(3, "koneksi database habis", "antrean pool menanjak");
System.out.println("== Urutan penanganan insiden ==");
String[] langkah = {
"1. amankan bukti: thread dump, heap dump, JFR, sebelum restart apa pun",
"2. pulihkan layanan: rollback atau scale-out dengan data yang ada",
"3. cari akar masalah lewat tabel keputusan di atas",
"4. tulis postmortem blameless (tanpa mencari kesalahan individu) beserta pencegahannya"
};
for (String l : langkah) {
System.out.println(" " + l);
}
}
}