Java Flight Recorder (JFR) adalah profiler built-in OpenJDK yang dirancang untuk berjalan terus-menerus di produksi dengan overhead sangat rendah. JDK Mission Control (JMC) adalah klien GUI yang membaca file rekaman JFR dan menampilkannya dalam bentuk grafik, tabel, dan flamegraph. Pada chapter ini kita akan membahas arsitektur JFR, cara memulai dan menghentikan rekaman, event-event yang terekam, serta workflow analisis dengan JMC.

Mengapa JFR Berbeda #

Berbeda dengan profiler tradisional yang memerlukan instrumentation atau sampling dengan overhead 5-30%, JFR didesain dari awal untuk continuous profiling dengan overhead di bawah 1%. Hal ini dimungkinkan oleh tiga desain:

  • Event-based, bukan sampling periodik. JFR merekam event spesifik (alokasi objek besar, GC, monitor wait, method compile) dengan biaya konstan per event.
  • Buffer lock-free: setiap thread memiliki buffer lokal, menulis tanpa sinkronisasi.
  • Threshold filter: event di bawah ambang batas (mis. durasi < 10 ms) dibuang sebelum ditulis.

Memulai Rekaman JFR #

JFR dikendalikan via flag -XX:StartFlightRecording saat startup atau jcmd saat runtime:

bash
# 1. Rekaman selama 60 detik saat startup
java -XX:StartFlightRecording=duration=60s,filename=/tmp/recording.jfr \
     -jar aplikasi.jar

# 2. Rekaman dengan kustomisasi (settings profile)
java -XX:StartFlightRecording=filename=/tmp/rec.jfr,settings=profile,delay=30s \
     -jar aplikasi.jar

# 3. Memulai rekaman saat aplikasi sudah berjalan
jcmd <PID> JFR.start name=profil-1 duration=300s filename=/tmp/rec.jfr

# 4. Dump rekaman yang sedang berjalan ke file tanpa menghentikan
jcmd <PID> JFR.dump name=profil-1 filename=/tmp/dump.jfr

# 5. Menghentikan rekaman lebih awal
jcmd <PID> JFR.stop name=profil-1

Event yang Terekam #

JFR mengkategorikan event menjadi beberapa grup:

GrupContoh eventKegunaan
jdk.GCPhasePauseGC pause, allocationAnalisis memory
jdk.SocketRead/WriteI/O jaringanLatensi jaringan
jdk.FileRead/WriteI/O fileBottleneck disk
jdk.LockMonitorWaitLock contentionThread contention
jdk.ExecutionSampleCPU sampleHot path CPU
jdk.ObjectAllocationSampleAlokasi objekHot path allocation
jdk.ThreadParkThread park/waitIdle/lock
jdk.JavaExceptionThrowException lemparanException storm

JDK Mission Control #

JMC adalah GUI untuk membaca file .jfr. Setelah file dibuka, JMC menampilkan beberapa tab:

  • General: ringkasan durasi, jumlah event, JVM args
  • JVM Internal: class loading, compilation, GC
  • Memory: heap usage, allocation rate per type
  • Lock instances: monitor contention
  • Method Profiling: top methods by CPU sample, flamegraph
  • I/O: read/write latency per file/socket

Tab Method Profiling dan Memory paling sering dipakai untuk investigasi performa. JMC juga mendukung plugin custom event type via Event Streaming API sejak JDK 14.

Demonstrasi Beban Kerja #

Kode berikut menghasilkan beban kerja yang akan terekam jelas di JFR. Tiga fase yang berbeda (alokasi, CPU, I/O) membuat hasil rekaman mudah dianalisis.

kotlin
data class Tugas(val id: Int, val judul: String, val selesai: Boolean)

fun main() {
    println("=== Simulasi Beban Kerja untuk JFR ===")

    // Phase 1: alokasi singkat (visible di allocation profile)
    val tugasList = (1..5).map { i ->
        Tugas(id = i, judul = "Tugas-$i", selesai = false)
    }
    println("Fase 1: membuat ${tugasList.size} objek Tugas")
    tugasList.take(2).forEach { println("  - $it") }

    // Phase 2: operasi CPU singkat (visible di CPU profile)
    println()
    println("Fase 2: hitung jumlah bilangan prima < 20")
    val prima = (2..19).filter { apakahPrima(it) }
    println("  Bilangan prima: $prima")
    println("  Jumlah: ${prima.size}")

    // Phase 3: I/O simulasi (visible di wall-clock profile)
    println()
    println("Fase 3: simulasi I/O singkat")
    Thread.sleep(10)
    println("  I/O selesai setelah 10 ms sleep")

    println()
    println("=== Yang Terekam oleh JFR ===")
    println("- Allocation: Tugas instances, lambda map/filter")
    println("- CPU       : apakahPrima() calls")
    println("- Wall      : Thread.sleep() di Fase 3")
    println("- GC        : minor GC saat heap terisi")
}

fun apakahPrima(n: Int): Boolean {
    if (n < 2) return false
    var i = 2
    while (i * i <= n) {
        if (n % i == 0) return false
        i++
    }
    return true
}

Output yang Dihasilkan:

text
=== Simulasi Beban Kerja untuk JFR ===
Fase 1: membuat 5 objek Tugas
  - Tugas(id=1, judul=Tugas-1, selesai=false)
  - Tugas(id=2, judul=Tugas-2, selesai=false)

Fase 2: hitung jumlah bilangan prima < 20
  Bilangan prima: [2, 3, 5, 7, 11, 13, 17, 19]
  Jumlah: 8

Fase 3: simulasi I/O singkat
  I/O selesai setelah 10 ms sleep

=== Yang Terekam oleh JFR ===
- Allocation: Tugas instances, lambda map/filter
- CPU       : apakahPrima() calls
- Wall      : Thread.sleep() di Fase 3
- GC        : minor GC saat heap terisi

Mari kita telaah kode di atas:

  1. Fase 1 (alokasi): map lambda menghasilkan Tugas instance. Event jdk.ObjectAllocationSample akan menangkap tipe ini dengan stack trace yang menunjuk ke main lambda.
  2. Fase 2 (CPU): apakahPrima dipanggil 18 kali. Event jdk.ExecutionSample akan menangkap method ini sebagai hot path CPU bila dijalankan lebih lama.
  3. Fase 3 (I/O wait): Thread.sleep(10) membuat thread masuk status TIMED_WAITING. Event jdk.ThreadPark akan tercatat.
  4. Ringkasan JFR: bagian akhir mencetak tipe event yang diharapkan muncul. Pembaca dapat membandingkan harapan dengan output JMC setelah menjalankan rekaman.

Continuous Profiling #

Untuk aplikasi produksi, JFR sering dipakai dengan durasi panjang dan rotasi file. Pendekatan ini disebut continuous profiling:

bash
# 30 menit per file, rotasi otomatis
java -XX:StartFlightRecording=maxsize=100M,maxage=30m,filename=/var/log/jfr/app.jfr \
     -jar aplikasi.jar

File .jfr lama akan otomatis di-rotate bila ukuran atau umur melewati batas. Untuk analisis insiden, ambil file .jfr yang mencakup waktu insiden dan buka di JMC.

Referensi Lanjutan #

Penutup #

JFR + JMC memberi kombinasi continuous profiling yang praktis untuk produksi. Overhead rendah memungkinkan JFR selalu aktif sehingga kita memiliki data historis saat insiden terjadi. Pada chapter terakhir section ini, kita akan membahas pola-pola performa memori yang umum dan cara mengoptimasi alokasi objek.