Mengoptimalkan aplikasi Scala berbasis data: menghindari alokasi berlebihan, memilih Array versus collection, memahami JVM warmup dan tuning GC, lalu profiling dengan JFR dan async-profiler serta benchmark presisi memakai JMH lewat sbt-jmh.

Setelah episode 20 membahas versi dan fitur, sekarang kita masuk ranah yang paling sering disalahpahami: performa. Banyak developer melakukan optimasi prematur — menebak-nebak mana yang lambat, menghindari fitur bahasa, atau menulis kode jelek demi kecepatan yang tidak terbukti. Episode ini mengajarkan pendekatan yang benar: ukur dulu, optimasi yang terbukti.
Mengapa penting? Karena aplikasi Scala yang lambat jarang karena Scala-nya, melainkan karena pola yang boros: alokasi berlebihan, struktur data yang salah, atau GC yang tidak dipahami. Dan sekali lagi, kecepatan tidak bisa ditebak — harus diukur dengan tool yang tepat.
Aturan emas performance engineering:
JVM sudah sangat cepat untuk kode yang ditulis wajar. Yang membunuh performa adalah pola tertentu yang mahal — dan itulah fokus kita.
Alokasi objek adalah musuh kinerja utama di JVM: semakin banyak objek, semakin sering GC bekerja. Contoh pola boros:
def process(numbers: Array[Int]): Int =
numbers.toList // salinan ke List
.map(_ * 2) // List baru
.filter(_ > 10) // List baru lagi
.sum // boxing ke objekUntuk data berukuran besar, setiap langkah mengalokasikan. Versi yang lebih hemat untuk koleksi besar:
def process(numbers: Array[Int]): Int =
var total = 0
var i = 0
while i < numbers.length do
val doubled = numbers(i) * 2
if doubled > 10 then total += doubled
i += 1
totalKedua versi benar; yang kedua jauh lebih hemat alokasi. Tetapi — dan ini penting — untuk ukuran data kecil (ratusan elemen), perbedaannya tak terukur. Optimasi semacam ini hanya layak setelah profiling membuktikan hot path-nya ada di sini.
Warning
Prioritas pertama harus tetap keterbacaan dan kebenaran. Menulis kode berantakan untuk kecepatan yang tidak diukur adalah kerugian ganda: susah dipelihara dan tidak terbukti lebih cepat. Alokasi-averse style dipakai hanya di hot path yang sudah terverifikasi.
Pilihan struktur data (episode 6) berdampak besar pada performa:
| Aspek | Array | List/Vector |
|---|---|---|
| Memori | Kontigu, tanpa boxing (untuk primitif) | Node terpisah / tree, boxing |
| Akses acak | O(1) | List O(n), Vector O(log n) |
| Alokasi | Minimum | Setiap operasi |
| Kapan dipakai | Hot path, data besar, interop Java | Kode idiomatis umum |
val data = Array.ofDim[Double](1_000_000)
var i = 0
while i < data.length do
data(i) = i * 1.5
i += 1Array[Double] menyimpan primitif secara kontigu — tanpa boxing, tanpa overhead. Ini pilihan untuk perhitungan numerik besar. Untuk kode bisnis biasa, List/Vector tetap lebih tepat: lebih aman, lebih ekspresif.
JVM melakukan JIT compilation — kode diinterpretasi dulu, lalu dioptimalkan saat sering dieksekusi. Konsekuensinya:
java -Xms2g -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-jar my-app.jarG1GC adalah default modern yang menyeimbangkan throughput dan pause. Untuk aplikasi dengan heap besar, -Xmx yang benar (tidak terlalu kecil → GC terus-menerus; tidak terlalu besar → pause panjang) adalah tuning pertama yang paling berdampak.
Jangan menebak — ukur. Dua tool utama di 2026:
JFR adalah profiler bawaan JDK, overhead sangat rendah:
java -XX:StartFlightRecording=filename=app.jfr,duration=60s -jar my-app.jarjfr summary app.jfr
jfr view alloc app.jfr # lihat alokasi terbesar
jfr view hot-methods app.jfrJFR menjawab pertanyaan kunci: metode mana yang paling banyak memakan CPU? Alokasi objek terbesar berasal dari mana? Dari sinilah kalian tahu hot path yang sebenarnya.
async-profiler memakai teknik perf_event_open + JVMTI untuk flame graph dengan overhead minimal:
./profiler.sh -d 30 -f flame.html <PID>Flame graph langsung menunjukkan stack frames yang dominan secara visual. Instalasi di Linux biasanya butuh perf_event_paranoid yang diatur:
sudo sysctl kernel.perf_event_paranoid=1JMH (Java Microbenchmark Harness) adalah standar benchmark JVM — ia mengatasi warmup, dead-code elimination, dan noise. Integrasi dengan sbt via plugin sbt-jmh:
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")enablePlugins(JmhPlugin)import org.openjdk.jmh.annotations.*
@State(Scope.Benchmark)
class SumBench:
val numbers: Array[Int] = (1 to 100_000).toArray
@Benchmark
def viaWhile(): Long =
var total = 0L
var i = 0
while i < numbers.length do
total += numbers(i)
i += 1
total
@Benchmark
def viaFold(): Long =
numbers.foldLeft(0L)(_ + _)Jalankan:
sbt "jmh:run -i 5 -wi 5 SumBench"JMH menjalankan 5 iterasi warmup dan 5 iterasi ukuran, lalu melaporkan throughput per benchmark:
Benchmark Mode Cnt Score Error Units
SumBench.viaFold thrpt 5 58325.343 ± 412.11 ops/s
SumBench.viaWhile thrpt 5 189034.221 ± 902.40 ops/sHasil ini baru berbicara — viaWhile terbukti ~3x lebih cepat untuk array besar. Dengan angka ini, keputusan optimasi bisa dipertanggungjawabkan.
Tip
Alur kerja yang benar: JMH untuk membandingkan dua implementasi secara adil, JFR/async-profiler untuk menemukan bottleneck di aplikasi nyata. Mulai dari profiling aplikasi (temukan masalah), lalu turun ke JMH (buktikan perbaikan) — bukan sebaliknya.
Blackhole/konsumsi hasil.-Xmx raksasa — heap besar bukan otomatis cepat; GC pause ikut membesar. Tuning butuh pengukuran, bukan insting.Inti yang harus dibawa pulang:
Array untuk data besar & numerik; List/Vector untuk kode idiomatis.-Xmx, G1) berdampak besar.Di episode 22, episode terakhir series ini, kita merangkum semuanya: ekosistem, alternatif, dan refleksi akhir — membandingkan Scala dengan Kotlin, Java, Haskell, dan Clojure, kapan memilih yang mana, plus checklist produksi dan sumber belajar resmi. Sampai jumpa di episode 22!