JMH (Java Microbenchmark Harness) adalah framework benchmark yang dikembangkan oleh tim OpenJDK. JMH menangani semua jebakan microbenchmark yang kita bahas di chapter sebelumnya secara otomatis: warmup iteration, dead code elimination via Blackhole, forked JVM untuk isolasi, dan statistik robust (median, percentil). Pada bab ini kita akan membedah struktur class JMH, konfigurasi Gradle untuk menjalankannya, dan annotation kunci yang paling sering dipakai. Demo memakai step-counting karena JMH nyata memerlukan setup Maven/Gradle.
Struktur Class JMH #
Class JMH terdiri dari tiga bagian: @State class yang menampung data shared, method @Benchmark yang diukur, dan annotation konfigurasi (@Warmup, @Measurement). Method benchmark dianotasi @Benchmark dan dipanggil otomatis oleh runner JMH.
@State(Scope.Benchmark)
class MyState {
val data: List<Int> = (1..1000).toList()
}
class MyBenchmark {
@Benchmark
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
fun sumList(state: MyState): Long {
return state.data.sumOf { it.toLong() }
}
}@State(Scope.Benchmark) membuat instance state disharing ke semua thread benchmark. Scope.Thread membuat instance per-thread. Pilihan scope memengaruhi validitas benchmark untuk kode concurrent.
Annotation Kunci #
| Annotation | Fungsi | Contoh |
|---|---|---|
@Benchmark | Menandai method yang diukur | @Benchmark fun sum() |
@State | Menandai class sebagai state shared | @State class MyState |
@Warmup | Konfigurasi warmup | @Warmup(iterations=5, time=1) |
@Measurement | Konfigurasi pengukuran | @Measurement(iterations=5, time=1) |
@Fork | Jumlah JVM fork | @Fork(1) |
@BenchmarkMode | Mode pengukuran | @BenchmarkMode(Mode.AverageTime) |
@OutputTimeUnit | Satuan waktu output | @OutputTimeUnit(TimeUnit.MICROSECONDS) |
@Fork penting karena setiap fork menjalankan JVM baru, sehingga JIT state bersih. Tanpa fork, hasil benchmark dapat dipengaruhi oleh JIT yang sudah aktif dari benchmark sebelumnya.
Demo Struktur JMH #
Karena JMH nyata memerlukan plugin Gradle/Maven, cuplikan berikut mendemokan struktur class dan simulasi runner. Simulasi menjalankan warmup dan measurement phase dengan step counter:
// Demo struktur JMH (Java Microbenchmark Harness).
// Mensimulasikan anotasi @Benchmark, @State, @Warmup, @Measurement
// dengan struktur yang menggambarkan fase-fase JMH.
annotation class Benchmark
annotation class State
annotation class Warmup(val iterations: Int, val time: Int)
annotation class Measurement(val iterations: Int, val time: Int)
@State
class MyState {
val data: List<Int> = (1..1000).toList()
}
class MyBenchmark {
@Benchmark
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
fun sumList(state: MyState): Long {
return state.data.sumOf { it.toLong() }
}
@Benchmark
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
fun sumSequence(state: MyState): Long {
return state.data.asSequence().sumOf { it.toLong() }
}
}
// Simulasi runner JMH yang menjalankan benchmark dengan fase
// warmup dan measurement. Output deterministik berupa langkah.
class JmhSimulator {
var totalOps: Long = 0
val results: MutableList<Pair<String, Long>> = mutableListOf()
fun run(name: String, warmupIter: Int, measureIter: Int, body: () -> Long) {
print("Benchmark $name: ")
// Warmup phase
var warmupOps = 0L
repeat(warmupIter) {
warmupOps += body()
}
// Measurement phase
var measureOps = 0L
repeat(measureIter) {
measureOps += body()
}
results.add(name to measureOps)
totalOps += measureOps
println("warmup=$warmupIter iters, measure=$measureIter iters, total ops=$measureOps")
}
fun report() {
println()
println("=== JMH Report ===")
results.forEach { (name, ops) ->
val bar = "#".repeat((ops * 20 / (results.maxOf { it.second } + 1)).toInt())
println(" %-25s ops/iter=%-10d [%s]".format(name, ops / 5, bar))
}
}
}
fun main() {
println("=== JMH (Java Microbenchmark Harness) ===")
println()
// 1. Struktur kelas benchmark
println("=== 1. Struktur Class Benchmark ===")
println("@State class MyState { val data = (1..1000).toList() }")
println()
println("class MyBenchmark {")
println(" @Benchmark")
println(" @Warmup(iterations = 5, time = 1)")
println(" @Measurement(iterations = 5, time = 1)")
println(" fun sumList(state: MyState): Long = state.data.sumOf { it.toLong() }")
println()
println(" @Benchmark")
println(" @Warmup(iterations = 5, time = 1)")
println(" @Measurement(iterations = 5, time = 1)")
println(" fun sumSequence(state: MyState): Long =")
println(" state.data.asSequence().sumOf { it.toLong() }")
println("}")
println()
// 2. Simulasi runner JMH
println("=== 2. Simulasi Runner JMH ===")
val sim = JmhSimulator()
val state = MyState()
sim.run("sumList", warmupIter = 5, measureIter = 5) { state.data.sumOf { it.toLong() } }
sim.run("sumSequence", warmupIter = 5, measureIter = 5) {
state.data.asSequence().sumOf { it.toLong() }
}
sim.report()
println()
println("Catatan: simulasi pakai step counter; JMH nyata melaporkan ops/detik.")
println()
// 3. Build configuration
println("=== 3. Build Configuration (Gradle) ===")
val gradleConfig = """
|plugins {
| kotlin("jvm") version "2.0.21"
| id("me.champeau.jmh") version "0.7.2"
|}
|
|jmh {
| fork = 1
| warmupIterations = 5
| iterations = 5
| warmupForks = 0
| timeOnIteration = "1s"
| benchmarkMode.set(listOf("avgt"))
| timeUnit.set("us")
| resultFormat.set("JSON")
|}
""".trimMargin()
println(gradleConfig)
println()
// 4. Annotation penjelasan
println("=== 4. Annotation Kunci JMH ===")
val annotations = listOf(
"@Benchmark" to "Menandai method yang akan diukur",
"@State" to "Menandai class sebagai state shared antar benchmark",
"@Warmup" to "Konfigurasi fase warmup (iterations, time)",
"@Measurement" to "Konfigurasi fase pengukuran (iterations, time)",
"@Fork" to "Jumlah JVM fork untuk isolasi pengukuran",
"@BenchmarkMode" to "Mode: avgt (average), thrpt (throughput), sample",
"@OutputTimeUnit" to "Satuan waktu output (us, ms, s)"
)
annotations.forEach { (ann, desc) ->
println(" %-25s $desc".format(ann))
}
}Output yang Dihasilkan:
=== JMH (Java Microbenchmark Harness) ===
=== 1. Struktur Class Benchmark ===
@State class MyState { val data = (1..1000).toList() }
class MyBenchmark {
@Benchmark
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
fun sumList(state: MyState): Long = state.data.sumOf { it.toLong() }
@Benchmark
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
fun sumSequence(state: MyState): Long =
state.data.asSequence().sumOf { it.toLong() }
}
=== 2. Simulasi Runner JMH ===
Benchmark sumList: warmup=5 iters, measure=5 iters, total ops=2502500
Benchmark sumSequence: warmup=5 iters, measure=5 iters, total ops=2502500
=== JMH Report ===
sumList ops/iter=500500 [###################]
sumSequence ops/iter=500500 [###################]
Catatan: simulasi pakai step counter; JMH nyata melaporkan ops/detik.
=== 3. Build Configuration (Gradle) ===
plugins {
kotlin("jvm") version "2.0.21"
id("me.champeau.jmh") version "0.7.2"
}
jmh {
fork = 1
warmupIterations = 5
iterations = 5
warmupForks = 0
timeOnIteration = "1s"
benchmarkMode.set(listOf("avgt"))
timeUnit.set("us")
resultFormat.set("JSON")
}
=== 4. Annotation Kunci JMH ===
@Benchmark Menandai method yang akan diukur
@State Menandai class sebagai state shared antar benchmark
@Warmup Konfigurasi fase warmup (iterations, time)
@Measurement Konfigurasi fase pengukuran (iterations, time)
@Fork Jumlah JVM fork untuk isolasi pengukuran
@BenchmarkMode Mode: avgt (average), thrpt (throughput), sample
@OutputTimeUnit Satuan waktu output (us, ms, s)Mari kita bedah bagian-bagian kunci dari kode di atas:
- Custom
@Benchmark,@State,@Warmup,@Measurement: pada JMH nyata, annotation ini berasal dari packageorg.openjdk.jmh.annotations. Di sini kita buat versi dummy agar kode dapat dikompilasi tanpa dependency JMH. Pada project nyata, tambahkanjmh-coreke dependency. JmhSimulator.run: function ini menjalankan dua fase. Warmup (5 iterasi) lalu measurement (5 iterasi). Hasil measurement yang dilaporkan, bukan warmup. Pola ini sama dengan JMH nyata.State.data: data 1000 elemen yang akan diproses di setiap iterasi. Pada JMH nyata, state di-setup sekali per@Setuplalu dipakai semua iterasi benchmark.- Gradle plugin
me.champeau.jmh: plugin resmi untuk JMH di Gradle. Setelah konfigurasi, jalankan./gradlew jmhdan hasil benchmark muncul dibuild/reports/jmh/results.json. fork = 1: setiap benchmark dijalankan di JVM fork terpisah, sehingga JIT state bersih. Untuk pengukuran serius, gunakanfork = 3agar hasil robust terhadap variasi startup JVM.
Menjalankan JMH di Gradle #
Setelah plugin terpasang, tempatkan class benchmark di folder src/jmh/kotlin. Plugin akan otomatis mendeteksi method @Benchmark dan menjalankannya saat ./gradlew jmh.
./gradlew jmh
# Hasil: build/reports/jmh/results.json
# build/reports/jmh/index.htmlHasil JSON dapat diproses lebih lanjut (misal dibandingkan dengan baseline untuk regression testing). Format HTML menyediakan visualisasi bar chart untuk perbandingan antar benchmark.
Benchmark Mode #
JMH mendukung beberapa mode pengukuran:
| Mode | Yang diukur | Cocok untuk |
|---|---|---|
AverageTime | Waktu rata-rata per operasi | Benchmark CPU-bound umum |
Throughput | Operasi per detik | Throughput server |
SampleTime | Distribusi waktu (percentile) | Latensi p99 |
SingleShotTime | Satu kali pengukuran | Cold start, startup time |
Pilihan mode bergantung pada pertanyaan yang ingin dijawab. Untuk “berapa cepat function A dibanding B”, AverageTime cukup. Untuk “berapa request per detik yang bisa dilayani”, Throughput lebih tepat.
Referensi Lanjutan #
- JMH Project — dokumentasi resmi JMH untuk membuat benchmark JVM
- JMH Source
— kode sumber JMH beserta contoh benchmark dan modul
jmh-core
Selanjutnya #
JMH memberi pengukuran akurat. Bab berikutnya membahas allocation profiling — teknik menemukan alokasi objek berlebih yang menyebabkan GC pressure. Materi tersebut ada di chapter Allocation Profiling.