Diagnostik JVM

Berkenalan dengan jcmd, jstack, thread dump, dan Flight Recorder untuk mendiagnosis program berjalan.

Daftar isi

Program Java yang sudah berjalan di server tidak bisa begitu saja dipasangi debugger, tetapi JVM menyertakan alat diagnostik yang dapat melihat ke dalamnya tanpa menghentikannya. Topik ini memperkenalkan jcmd sebagai pintu masuk utamanya, cara membaca thread dump, mendeteksi deadlock, dan Flight Recorder untuk merekam perilaku program. Semua konsepnya dipraktikkan dengan program yang bisa dijalankan di terminal, sehingga isi laporan alat tersebut terbaca dengan dasar yang kokoh.

Mendiagnosis program yang masih berjalan#

Gejala di lingkungan produksi jarang datang berupa exception yang rapi. Gejalanya biasa berupa program yang lambat, thread yang berhenti merespons, atau pemakaian memori yang terus naik, dan semua itu terjadi saat program sedang hidup. Alat diagnostik JVM bekerja pada proses yang sedang berjalan, membaca keadaannya, lalu mencetak laporan tanpa mengubah jalannya program.

Pemilihan alatnya mengikuti gejala yang dialami, dan pemetaan paling sering dipakai digambarkan berikut ini.

Program macet saling menungguthread dumpjcmd PID Thread.printProgram berjalan lambatrekaman Flight Recorderjcmd PID JFR.startPemakaian memori terus naikheap dumpjcmd PID GC.heap_dump
Pemetaan gejala ke alat diagnostik JVM yang tepat

JDK modern menempatkan jcmd sebagai alat yang paling lengkap, karena satu perintah ini bisa memanggil hampir semua kebutuhan diagnostik. Alat lama seperti jstack untuk thread dump dan jmap untuk heap dump masih tersedia, tetapi fungsinya kini dapat dipanggil lewat jcmd, sehingga daftar perintah yang perlu dihafal menjadi pendek. Perilaku thread dan pembekuan antar-thread sendiri sudah dibahas pada topik Sinkronisasi, dan topik ini menyambungnya dengan cara mengamati perilaku tersebut dari luar program.

Satu persyaratan praktis perlu diketahui sejak awal, yaitu jcmd harus dijalankan di komputer yang sama dengan prosesnya dan oleh pengguna yang sama dengan pemilik proses JVM. Dengan syarat itu, semua aksi di bawah ini bekerja pada program apa pun tanpa perlu mengubah satu baris kode atau menjalankan ulang apa pun.

jcmd: satu pintu untuk banyak kebutuhan#

jcmd menerima PID proses JVM sebagai argumen pertama, disusul nama aksinya, dan mencetak laporan langsung ke terminal. Pemakaian pertamanya biasanya menemukan PID-nya sendiri, karena jcmd tanpa argumen mencantumkan semua proses JVM yang sedang hidup di komputer tersebut. Perintah-perintah berikut menggambarkan pekerjaan diagnostik yang paling sering dilakukan.

bash
# tidak-dijalankan
# mencantumkan semua proses JVM yang berjalan beserta PID-nya
jcmd

# melihat properti sistem dan bendera JVM yang aktif pada proses 12345
jcmd 12345 VM.system_properties
jcmd 12345 VM.flags

# membuat thread dump dari proses yang sedang hidup
jcmd 12345 Thread.print

# membuat heap dump ke berkas untuk dianalisis kemudian
jcmd 12345 GC.heap_dump /tmp/heap-dump.hprof

Bentuk pemakaiannya selalu sama, sehingga menambah perintah baru tidak menambah kebingungan. Di komputer pembaca, angka 12345 diganti dengan PID yang tercetak pada jcmd pertama. Bila butuh daftar lengkapnya, aksi help mencetak seluruh aksi yang tersedia untuk proses tersebut, dan daftarnya bisa berbeda antarversi JDK karena jcmd memang tumbuh mengikuti JVM-nya.

bash
# tidak-dijalankan
# menampilkan semua aksi yang tersedia untuk proses tertentu
jcmd 12345 help

Perlu dicatat bahwa aksi seperti Thread.print aman terhadap program yang sedang hidup, sedangkan GC.heap_dump memicu jeda singkat sejenak di safepoint, sehingga pada layanan yang sangat sibuk sebaiknya dijalankan dengan pertimbangan.

Membaca thread dump#

Thread dump adalah potret keadaan semua thread pada satu momen, berisi nama thread, thread state, dan tumpukan pemanggilan (stack trace) masing-masing. Dari laporan itu terbaca thread mana yang sedang sibuk, mana yang menunggu, dan mana yang saling menunggu. Keadaan thread yang paling sering ditemui ada empat. RUNNABLE berarti thread sedang mengerjakan sesuatu, BLOCKED berarti thread menunggu kunci monitor, WAITING berarti thread menunggu tanpa batas waktu, dan TIMED_WAITING berarti thread menunggu dengan batas waktu.

Keempat keadaan itu dapat diperagakan dengan program kecil berikut, yang membuat tiga thread berada dalam keadaan berbeda lalu membacanya lewat getState().

java PetaStateThread.java
public class PetaStateThread {

    static final Object MONITOR = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread baru = new Thread(() -> {}, "thread-baru");

        Thread tidur = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "thread-tidur");

        Thread tunggu = new Thread(() -> {
            synchronized (MONITOR) {
                try {
                    MONITOR.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "thread-tunggu");

        System.out.println("sebelum start: " + baru.getName() + " = " + baru.getState());
        tidur.start();
        tunggu.start();
        Thread.sleep(100);
        System.out.println("saat berjalan: " + tidur.getName() + " = " + tidur.getState());
        System.out.println("saat berjalan: " + tunggu.getName() + " = " + tunggu.getState());
        tidur.interrupt();
        tunggu.interrupt();
        System.exit(0);
    }
}
text Keluaran
sebelum start: thread-baru = NEW
saat berjalan: thread-tidur = TIMED_WAITING
saat berjalan: thread-tunggu = WAITING

Keluaran tersebut adalah versi mini dari apa yang dicetak jcmd Thread.print. Thread yang belum dijalankan berkeadaan NEW, thread yang menjalankan Thread.sleep(5000) masuk ke TIMED_WAITING karena menunggu dengan batas waktu, dan thread yang menjalankan wait() masuk ke WAITING karena menunggu tanpa batas. Pada thread dump sungguhan, setiap keadaan ini disertai tumpukan pemanggilan lengkap, sehingga baris kode tempat thread berhenti pun terbaca.

Mendeteksi deadlock#

Deadlock terjadi ketika dua thread atau lebih saling menunggu kunci yang dipegang oleh satu sama lain, sehingga tidak ada yang bisa melanjutkan. Kondisi ini biasanya lahir dari urutan pengambilan kunci yang berbeda antara dua thread, dan thread dump adalah alat utama untuk menangkapnya di program hidup. Program berikut membangun deadlock sungguhan lalu mendeteksinya dengan ThreadMXBean, yaitu antarmuka manajemen JVM yang dipakai juga oleh alat diagnostik.

java DeteksiDeadlock.java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.Comparator;

public class DeteksiDeadlock {

    static final Object KUNCI_A = new Object();
    static final Object KUNCI_B = new Object();

    static void tidur(int milidetik) {
        try {
            Thread.sleep(milidetik);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread satu = new Thread(() -> {
            synchronized (KUNCI_A) {
                tidur(200);
                synchronized (KUNCI_B) {
                    System.out.println("thread satu tuntas");
                }
            }
        }, "pembayar-tagihan");

        Thread dua = new Thread(() -> {
            synchronized (KUNCI_B) {
                tidur(200);
                synchronized (KUNCI_A) {
                    System.out.println("thread dua tuntas");
                }
            }
        }, "pengirim-laporan");

        satu.start();
        dua.start();
        Thread.sleep(500);

        ThreadMXBean mx = ManagementFactory.getThreadMXBean();
        long[] terkunci = mx.findDeadlockedThreads();
        if (terkunci == null) {
            System.out.println("tidak ada deadlock terdeteksi");
        } else {
            ThreadInfo[] info = mx.getThreadInfo(terkunci);
            Arrays.sort(info, Comparator.comparing(ThreadInfo::getThreadName));
            System.out.println("deadlock terdeteksi pada " + info.length + " thread:");
            for (ThreadInfo t : info) {
                System.out.println("  " + t.getThreadName() + " : " + t.getThreadState());
            }
        }
        System.exit(0);
    }
}
text Keluaran
deadlock terdeteksi pada 2 thread:
  pembayar-tagihan : BLOCKED
  pengirim-laporan : BLOCKED

Cara kerjanya mudah ditelusuri. Thread pembayar-tagihan memegang KUNCI_A lalu meminta KUNCI_B, sedangkan thread pengirim-laporan memegang KUNCI_B lalu meminta KUNCI_A, dan keduanya terjebak dalam keadaan BLOCKED selamanya. Jeda 200 milidetik di antara dua pengambilan kunci memperbesar jendela kejadian, sehingga deadlock hampir selalu terbentuk saat program ini dijalankan.

Pada thread dump sungguhan, bagian akhir laporan jcmd Thread.print biasanya memuat blok yang menyatakan deadlock ditemukan, lengkap dengan kedua thread serta kunci yang saling ditunggu, sebagaimana dijelaskan panduan penelusuran masalah JVM di bagian Referensi. Deteksi programatik di atas memakai mekanisme yang sama dengan alat tersebut, yaitu memeriksa grafik tunggu antar-thread, dan cukup dipanggil sekali karena deadlock yang sejati tidak akan hilang dengan sendirinya.

Flight Recorder#

Flight Recorder (JFR) adalah perekam peristiwa bawaan JVM yang menampung data performa secara terus-menerus dengan beban yang sangat rendah. Yang terekam mencakup jeda garbage collection, alokasi memori, pemakaian CPU per metode, I/O berkas dan jaringan, sampai kejadian kunci antar-thread, dan semuanya tersimpan di dalam proses itu sendiri. Pada JDK modern, JFR tersedia tanpa biaya dan dianggap aman dinyalakan sejak program dimulai.

Rekaman diambil lewat jcmd dengan aksi JFR.start dan JFR.stop, dan JFR.start menerima durasi serta nama berkas keluaran berformat .jfr. Alternatif lain adalah menyalakan perekaman sejak awal dengan bendera -XX:StartFlightRecording pada baris perintah java. Berkas rekamannya kemudian dibuka di JDK Mission Control, aplikasi resmi untuk melihat ringkasan jeda, metode terpanas, dan rentetan kejadian sepanjang waktu.

Perbedaannya dengan thread dump penting untuk dipahami. Thread dump adalah potret satu momen yang menjawab “apa yang sedang terjadi sekarang”, sedangkan JFR adalah rekaman sejarah yang menjawab “apa yang terjadi selama satu jam terakhir”. Keduanya saling melengkapi, yaitu JFR menemukan rentang waktu bermasalah, lalu thread dump mengambil potret pada rentang yang dicurigai.

Kesalahan umum#

  • Mengambil thread dump hanya satu kali. Satu potret menangkap keadaan sesaat, sedangkan pola tersangkut hanya terlihat pada beberapa potret berjarak beberapa detik. Ambil tiga sampai lima thread dump berurutan sebelum menyimpulkan.
  • Menganggap program yang macet pasti crash. Deadlock tidak menghentikan proses, dan programnya masih hidup dengan beberapa thread yang diam selamanya. Periksa thread dump sebelum menyalakan ulang layanan.
  • Menyimpulkan penyebab lambat hanya dari thread dump. Potret thread memperlihatkan siapa yang berhenti, tetapi tidak menjelaskan profil sepanjang waktu. Gunakan JFR untuk pertanyaan mengenai sebaran waktu.
  • Mengabaikan pemberitahuan deadlock dari laporan. Blok deadlock pada thread dump sudah menyebutkan kunci dan thread yang terlibat, sehingga titik perbaikannya jelas. Menyalakan ulang tanpa membaca laporan berarti bug yang sama akan kembali.
  • Membiarkan rekaman JFR dengan konfigurasi terlalu rinci di lingkungan produksi tanpa melihat beban. Perekaman profil penuh untuk jangka panjang memerlukan kapasitas berkas dan analisis yang serius. Mulailah dari konfigurasi bawaan.

Ringkasan#

  • jcmd adalah alat diagnostik JVM untuk proses yang berjalan, dan hampir semua kebutuhan laporan dapat dipanggil darinya.
  • Thread dump memuat nama, keadaan, dan tumpukan setiap thread, dengan keadaan utama berupa RUNNABLE, BLOCKED, WAITING, dan TIMED_WAITING.
  • Deadlock terlihat sebagai dua thread BLOCKED yang saling menunggu kunci, dan JVM dapat mendeteksinya lewat ThreadMXBean maupun laporan thread dump.
  • Flight Recorder merekam peristiwa performa sepanjang waktu dengan beban rendah, sehingga melengkapi potret sesaat dari thread dump.

Referensi#