Saat aplikasi Kotlin/JVM berjalan lebih lambat dari yang diharapkan atau mengonsumsi memori lebih besar dari prediksi, kita memerlukan visibilitas ke dalam runtime. Profiling adalah kegiatan mengukur aspek-aspek tersebut — waktu eksekusi method, alokasi objek, dan jejak CPU. Tanpa profiler, optimasi yang kita lakukan sering kali bersifat spekulatif. Pada chapter ini kita akan mengenal alat profiling utama untuk JVM, perbedaan sampler vs profiler, serta cara memilih jenis pengukuran yang tepat.

Kategori Profiler #

Profiler JVM umumnya bekerja dengan dua teknik pengambilan data: instrumentation dan sampling.

TeknikCara kerjaKelebihanKekurangan
InstrumentationMemasang hook ke setiap method (via bytecode rewriting)Akurat per-callOverhead besar, mengubah profil
SamplingMengambil snapshot stack thread periodik (mis. 10 ms)Overhead rendah, tidak mengubah profilKurang akurat untuk method cepat

Untuk pekerjaan produksi, sampling hampir selalu menjadi pilihan. Instrumentasi dipakai saat kita perlu akurasi absolut pada method tertentu (mis. unit test microbenchmark).

Alat Utama #

Empat alat berikut paling sering dipakai pada ekosistem JVM:

  • VisualVM: GUI gratis dari Oracle (sekarang komunitas). Menggabungkan CPU sampler, memory profiler, heap dump viewer, dan thread monitor. Cocok untuk pengembangan.
  • async-profiler: profiler low-overhead berbasis perf_events Linux. Menghasilkan flamegraph dalam format SVG. Tidak mengubah profil karena tidak memakai instrumentation.
  • Java Flight Recorder (JFR): profiler built-in di OpenJDK 11+. Overhead sangat rendah (< 1% pada beban normal). Cocok untuk continuous profiling.
  • IntelliJ Profiler: integrasi async-profiler dan JFR di dalam IDE. Memungkinkan profil langsung dari run configuration.

VisualVM #

VisualVM dapat dipasang secara terpisah atau dipakai sebagai plugin IntelliJ. Setelah aplikasi target berjalan, VisualVM menampilkan proses JVM lokal dan remote. Tab utama:

text
Overview  -> info versi, JVM args, system properties
Monitor   -> grafik CPU, heap, threads, classes real-time
Threads   -> status setiap thread (runnable, waiting, blocked)
Sampler   -> CPU sample dan memory sample
Profiler  -> instrumentation mode (memerlukan restart target)

Untuk memeriksa alokasi, pakai tab Sampler → Memory → Take snapshot. Snapshot dapat disimpan sebagai .hprof dan dianalisis kemudian.

async-profiler #

async-profiler berjalan sebagai agen native via -agentpath. Output utama adalah flamegraph yang menampilkan tumpukan pemanggilan dengan lebar proporsional terhadap waktu.

bash
# Memasang agen dan menghasilkan flamegraph SVG
java -agentpath:/opt/async-profiler/libasyncProfiler.so=start,event=cpu,file=/tmp/profile.svg \
     -jar aplikasi.jar

# Mengubah event menjadi alokasi
java -agentpath:/opt/async-profiler/libasyncProfiler.so=start,event=alloc,file=/tmp/alloc.svg \
     -jar aplikasi.jar

# Event lain yang didukung: cache-misses, lock, page-faults, context-switches

Java Flight Recorder #

JFR adalah bagian dari OpenJDK sejak versi 11. Profil disimpan dalam format biner .jfr yang dapat dianalisis dengan JDK Mission Control (JMC) atau di-parse programatik.

bash
# Memulai JFR selama 60 detik
java -XX:StartFlightRecording=duration=60s,filename=/tmp/recording.jfr \
     -jar aplikasi.jar

# Atau memulai JFR saat aplikasi sudah berjalan
jcmd <PID> JFR.start duration=60s filename=/tmp/recording.jfr

# Menghentikan recording lebih awal
jcmd <PID> JFR.stop name=1

Demonstrasi Beban Kerja #

Kode berikut menghasilkan beban alokasi yang nyata. Saat diprofil, kode ini akan memperlihatkan pola yang khas: alokasi singkat di young generation diikuti pengumpulan oleh minor GC.

kotlin
data class Pengguna(val id: Int, val nama: String, val email: String)

fun main() {
    println("=== Skenario Beban Alokasi Objek ===")

    // Membuat banyak objek pengguna untuk simulasi workload
    val daftar = mutableListOf<Pengguna>()
    repeat(10) { i ->
        daftar.add(Pengguna(id = i, nama = "Pengguna-$i", email = "pengguna-$i@example.com"))
    }

    println("Membuat ${daftar.size} objek Pengguna")
    println("Sample objek pertama: ${daftar.first()}")
    println("Sample objek terakhir: ${daftar.last()}")

    // Menghitung total alokasi memori (estimasi kasar)
    println()
    println("=== Estimasi Alokasi ===")
    println("Setiap Pengguna ≈ 40 byte (header) + 4 byte (Int) + 2 ref (8 byte) = ~60 byte")
    println("Total 10 objek ≈ 600 byte + String literals di constant pool")

    // Profiling pattern: cek seberapa cepat GC bekerja
    println()
    println("=== Pola Yang Bagus untuk Profiling ===")
    println("1. Hot path: alokasi massal pada loop singkat")
    println("2. Steady state: jumlah objek stabil selama waktu")
    println("3. Pressure: alokasi terus tumbuh tanpa batas (indikasi leak)")

    println()
    println("=== Alat Profiling yang Direkomendasikan ===")
    println("- VisualVM           : GUI gratis, sampler CPU + memory")
    println("- async-profiler     : low-overhead, flamegraph")
    println("- Java Flight Recorder: low-overhead, continuous")
    println("- IntelliJ Profiler  : integrasi IDE")
}

Output yang Dihasilkan:

text
=== Skenario Beban Alokasi Objek ===
Membuat 10 objek Pengguna
Sample objek pertama: Pengguna(id=0, nama=Pengguna-0, [email protected])
Sample objek terakhir: Pengguna(id=9, nama=Pengguna-9, [email protected])

=== Estimasi Alokasi ===
Setiap Pengguna ≈ 40 byte (header) + 4 byte (Int) + 2 ref (8 byte) = ~60 byte
Total 10 objek ≈ 600 byte + String literals di constant pool

=== Pola Yang Bagus untuk Profiling ===
1. Hot path: alokasi massal pada loop singkat
2. Steady state: jumlah objek stabil selama waktu
3. Pressure: alokasi terus tumbuh tanpa batas (indikasi leak)

=== Alat Profiling yang Direkomendasikan ===
- VisualVM           : GUI gratis, sampler CPU + memory
- async-profiler     : low-overhead, flamegraph
- Java Flight Recorder: low-overhead, continuous
- IntelliJ Profiler  : integrasi IDE

Mari kita telaah kode di atas:

  1. data class Pengguna: deklarasi ringkas yang menghasilkan class dengan field, constructor, equals, hashCode, toString, dan copy. Inilah pola class yang paling sering muncul di flamegraph alokasi.
  2. repeat(10): loop singkat yang mengalokasikan 10 instance. Pada skala produksi, loop 10.000 atau 100.000 iterasi akan terlihat jelas di flamegraph sebagai sumber alokasi utama.
  3. Estimasi alokasi: angka ~60 byte per objek adalah perkiraan untuk class dengan satu Int dan dua String. Angka sebenarnya tergantung pada compressed oops dan ukuran header objek.
  4. Tiga pola profil: pembaca diarahkan untuk memilih jenis pengukuran (CPU/memory/allocation) berdasarkan pola gejala yang dihadapi.

Memilih Mode Profiling #

GejalaMode profiling yang sesuai
Throughput rendah, CPU 100%CPU sample (async-profiler event=cpu)
Latensi tinggi pada beberapa requestCPU sample + wall-clock (event=wall)
GC sering, heap cepat penuhAllocation profile (event=alloc)
Lock contention, thread waitingLock profile (event=lock)
Memory leak bertumbuh pelanHeap dump + retained size analysis

Referensi Lanjutan #

  • JetBrains Profiler — panduan IntelliJ Profiler untuk CPU, memori, dan thread profiling
  • JVM Guide — panduan HotSpot JVM Java SE 21 sebagai dasar observasi runtime

Penutup #

Pemilihan alat profiling yang tepat menentukan apakah kita menginvestigasi masalah dalam hitungan menit atau berhari-hari. VisualVM cocok untuk pengembangan, async-profiler untuk flamegraph produksi, dan JFR untuk continuous profiling. Pada chapter berikutnya kita akan membahas JFR lebih dalam beserta alat analisisnya, JDK Mission Control.