Aplikasi terasa lambat, dan tebakan pertama kita hampir selalu salah: kode yang “tampak berat” sering bukan penyebabnya, sementara hotspot sesungguhnya bersembunyi di method yang tidak pernah kita curigai. Profiling adalah seni mengamati program secara objektif — mengumpulkan data tentang ke mana CPU, memori, dan waktu tunggu benar-benar pergi — sebelum menyentuh satu baris kode pun. Section ini membuka dengan alur kerja profiling yang benar, alat JDK bawaan yang siap pakai, dan cara membaca hasilnya menjadi daftar sasaran optimasi yang berurutan.
Ukur Dulu, Ubah Kemudian #
Alur kerja optimasi yang sehat selalu berbentuk lingkaran: ukur, temukan sasaran teratas, ubah, lalu ukur ulang untuk membuktikan dampaknya. Melompat langsung ke langkah “ubah” menghasilkan dua risiko nyata — perubahan bisa gagal menyasar hotspot sebenarnya, atau perubahan justru memperlambat bagian lain yang tidak terukur. Profiling menjawab tiga pertanyaan berbeda, dan setiap pertanyaan punya jenis data profilnya sendiri:
| Pertanyaan | Jenis profil | Data yang dikumpulkan |
|---|---|---|
| Ke mana waktu CPU pergi? | CPU profile | sampel stack thread yang sibuk |
| Di mana objek diciptakan? | allocation profile | jejak alokasi per lokasi kode |
| Apa yang menahan memori? | heap profile | peta referensi objek yang masih hidup |
| Siapa menunggu siapa? | contention profile | kejadian masuk-keluar monitor dan lock |
Alat JDK Bawaan #
Kita tidak perlu memasang apa pun untuk memulai. JDK 25 sudah membawa jcmd (perintah diagnostik ke JVM yang hidup) dan jfr (pembaca rekaman Java Flight Recorder). Perintah berikut kita jalankan terhadap proses Java contoh yang mengalokasikan kira-kira 80 MB:
jcmd <pid> GC.heap_infogarbage-first heap total reserved 262144K, committed 262144K, used 84397K [0x00000000f0000000, 0x0000000100000000)
region size 1024K, 2 young (2048K), 0 survivors (0K)Satu baris itu sudah menjawab pertanyaan heap: ukuran total, terpakai, dan ukuran region G1. Untuk inventaris per class, jcmd <pid> GC.class_histogram mengurutkan class berdasarkan byte yang dipakai — pada proses uji di atas, baris teratas adalah [J (array long) dengan 45 instance sebesar 80.001.960 byte, persis cocok dengan 40 array dua juta byte yang sengaja dialokasikan program. Rekaman JFR menambah dimensi waktu: jfr summary rekaman.jfr mencantumkan event type beserta jumlah dan ukurannya, dan jfr print --events jdk.CPULoad rekaman.jfr mengeluarkan seri beban CPU per detik. Mekanika tiap alat dibedah tuntas pada Chapter JFR dan JDK Mission Control dan Chapter JCMD, JStack, dan JMap di section JVM dan Memory Management — di sini fokus kita adalah alur kerjanya.
Sampling di Atas Kertas #
Cara kerja profiler sampling paling mudah dipahami lewat model yang bisa kita hitung sendiri. Program berikut memodelkan pohon panggilan dengan “satuan kerja” per method — bukan milidetik — lalu menyalakan sampler setiap 5 satuan dan mencatat method terdalam yang sedang bekerja:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Simulasi cara profiler sampling mengubah "satuan kerja" menjadi tabel
* porsi beban per method. Semua angka berupa penghitung operasi murni,
* bukan hasil pengukuran waktu, sehingga output selalu sama.
*/
public class PetaProfil {
static final int INTERVAL_SAMPLING = 5; // 1 sampel tiap 5 satuan kerja
static int unitBerjalan = 0;
static final class Metode {
final String nama;
final int unitKerja;
final List<Metode> anak = new ArrayList<>();
int sampel = 0; // porsi eksklusif: satuan di method ini sendiri
int inklusif = 0; // porsi inklusif: method ini + seluruh pemanggilan anak
Metode(String nama, int unitKerja) {
this.nama = nama;
this.unitKerja = unitKerja;
}
Metode panggil(Metode anak_) {
anak.add(anak_);
return this;
}
}
public static void main(String[] args) {
Metode akar = bangunPanggilan();
// Eksekusi simulasi: setiap satuan kerja dikerjakan, sampler menyala
// setiap kelipatan INTERVAL_SAMPLING dan mencatat method terdalam.
jalankan(akar);
hitungInklusif(akar);
int totalUnit = akar.inklusif;
List<Metode> semua = new ArrayList<>();
kumpulkan(akar, semua);
System.out.println("Model eksekusi (satuan kerja, bukan milidetik):");
cetakPohon(akar, 0);
System.out.println();
System.out.println("Total satuan kerja = " + totalUnit
+ ", interval sampling = " + INTERVAL_SAMPLING
+ " -> total sampel = " + (totalUnit / INTERVAL_SAMPLING));
System.out.println();
semua.sort(Comparator.comparingInt((Metode m) -> -m.sampel)
.thenComparing(m -> m.nama));
System.out.printf("%-20s %8s %8s %12s%n", "method", "sampel", "unit", "porsi");
for (Metode m : semua) {
double porsi = 100.0 * m.sampel * INTERVAL_SAMPLING / totalUnit;
System.out.printf("%-20s %8d %8d %11.1f%%%n",
m.nama, m.sampel, m.unitKerja, porsi);
}
System.out.println();
semua.sort(Comparator.comparingInt((Metode m) -> -m.inklusif)
.thenComparing(m -> m.nama));
System.out.printf("%-20s %10s%n", "method", "inklusif");
for (Metode m : semua) {
System.out.printf("%-20s %10d%n", m.nama, m.inklusif);
}
System.out.println();
System.out.println("Sasaran optimasi (porsi eksklusif >= 20%):");
for (Metode m : semua) {
if (100.0 * m.sampel * INTERVAL_SAMPLING / totalUnit >= 20) {
System.out.println(" - " + m.nama + ": periksa algoritma dan sumber daya");
}
}
}
static Metode bangunPanggilan() {
return new Metode("main", 2).panggil(
new Metode("prosesPesanan", 3)
.panggil(new Metode("validasiPesanan", 4))
.panggil(new Metode("hitungTotal", 10)
.panggil(new Metode("diskonBertingkat", 30)))
.panggil(new Metode("simpanKeBasisData", 6)
.panggil(new Metode("koneksiBaru", 25))));
}
static void jalankan(Metode m) {
for (int u = 0; u < m.unitKerja; u++) {
unitBerjalan++;
if (unitBerjalan % INTERVAL_SAMPLING == 0) {
m.sampel++;
}
}
for (Metode a : m.anak) {
jalankan(a);
}
}
static int hitungInklusif(Metode m) {
int total = m.unitKerja;
for (Metode a : m.anak) {
total += hitungInklusif(a);
}
m.inklusif = total;
return total;
}
static void kumpulkan(Metode m, List<Metode> wadah) {
wadah.add(m);
for (Metode a : m.anak) {
kumpulkan(a, wadah);
}
}
static void cetakPohon(Metode m, int kedalaman) {
System.out.println(" ".repeat(kedalaman) + m.nama + " (" + m.unitKerja + ")");
for (Metode a : m.anak) {
cetakPohon(a, kedalaman + 1);
}
}
}Output yang Dihasilkan:
Model eksekusi (satuan kerja, bukan milidetik):
main (2)
prosesPesanan (3)
validasiPesanan (4)
hitungTotal (10)
diskonBertingkat (30)
simpanKeBasisData (6)
koneksiBaru (25)
Total satuan kerja = 80, interval sampling = 5 -> total sampel = 16
method sampel unit porsi
diskonBertingkat 6 30 37.5%
koneksiBaru 5 25 31.3%
hitungTotal 2 10 12.5%
simpanKeBasisData 2 6 12.5%
prosesPesanan 1 3 6.3%
main 0 2 0.0%
validasiPesanan 0 4 0.0%
method inklusif
main 80
prosesPesanan 78
hitungTotal 40
simpanKeBasisData 31
diskonBertingkat 30
koneksiBaru 25
validasiPesanan 4
Sasaran optimasi (porsi eksklusif >= 20%):
- diskonBertingkat: periksa algoritma dan sumber daya
- koneksiBaru: periksa algoritma dan sumber dayaMari kita telaah baris-baris kuncinya:
- Baris 79-87 (
bangunPanggilan): pohon panggilan dibangun sebagai data —mainmemanggilprosesPesanan, yang memanggil tiga method lain, termasukdiskonBertingkat(30 satuan) dankoneksiBaru(25 satuan). - Baris 89-99 (
jalankan): eksekusi simulasi mengerjakan satuan satu per satu; setiap kelipatanINTERVAL_SAMPLING, sampler menaikkan penghitung method yang sedang bekerja. Inilah mekanisme sampling profiler sungguhan: biaya tercatat pada method terdalam di stack. - Baris 101-108 (
hitungInklusif): porsi inklusif dihitung rekursif — satuan method sendiri ditambah seluruh anaknya. Angka ini menjawab “berapa mahal kalau method ini dipanggil”, sementara porsi eksklusif menjawab “berapa mahal isi method ini sendiri”. - Baris 53-59 (tabel porsi): hasil diurutkan menurun lalu dicetak dengan
printfagar rapi; porsi dihitung dari sampel, bukan satuan — sama seperti profiler nyata yang hanya mengenal sampel.
Membaca Hasil Menjadi Sasaran #
Dua nama muncul jauh di atas yang lain: diskonBertingkat (37,5%) dan koneksiBaru (31,3%). Tabel inklusif menambahkan konteks: jalur simpanKeBasisData menyerap 31 satuan, hampir semuanya dari koneksiBaru. Pola seperti ini adalah sinyal klasik — method pembuat koneksi yang dipanggil sekali per penyimpanan. Saran optimasinya mengikuti data: hitung ulang algoritma diskon bertingkat, dan gunakan ulang koneksi alih-alih membuat baru setiap kali. Tanpa profil, kita mungkin sibuk mempercepat hitungTotal yang porsinya hanya 12,5%.
Dengan sasaran terukur di tangan, pertanyaan berikutnya adalah alat yang mengubah perubahan kode menjadi angka yang bisa dipercaya. Chapter berikutnya membahas fondasi benchmarking: jebakan apa saja yang menunggu pengukuran waktu yang naif, dan bagaimana menyusun pengukuran yang jujur.
Referensi #
- Oracle: jcmd — JDK 25 Tool Reference
- Oracle: jfr — JDK 25 Tool Reference
- Oracle: Troubleshooting — Diagnostic Tools
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PetaProfil.java
Unduh berkasimport java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Simulasi cara profiler sampling mengubah "satuan kerja" menjadi tabel
* porsi beban per method. Semua angka berupa penghitung operasi murni,
* bukan hasil pengukuran waktu, sehingga output selalu sama.
*/
public class PetaProfil {
static final int INTERVAL_SAMPLING = 5; // 1 sampel tiap 5 satuan kerja
static int unitBerjalan = 0;
static final class Metode {
final String nama;
final int unitKerja;
final List<Metode> anak = new ArrayList<>();
int sampel = 0; // porsi eksklusif: satuan di method ini sendiri
int inklusif = 0; // porsi inklusif: method ini + seluruh pemanggilan anak
Metode(String nama, int unitKerja) {
this.nama = nama;
this.unitKerja = unitKerja;
}
Metode panggil(Metode anak_) {
anak.add(anak_);
return this;
}
}
public static void main(String[] args) {
Metode akar = bangunPanggilan();
// Eksekusi simulasi: setiap satuan kerja dikerjakan, sampler menyala
// setiap kelipatan INTERVAL_SAMPLING dan mencatat method terdalam.
jalankan(akar);
hitungInklusif(akar);
int totalUnit = akar.inklusif;
List<Metode> semua = new ArrayList<>();
kumpulkan(akar, semua);
System.out.println("Model eksekusi (satuan kerja, bukan milidetik):");
cetakPohon(akar, 0);
System.out.println();
System.out.println("Total satuan kerja = " + totalUnit
+ ", interval sampling = " + INTERVAL_SAMPLING
+ " -> total sampel = " + (totalUnit / INTERVAL_SAMPLING));
System.out.println();
semua.sort(Comparator.comparingInt((Metode m) -> -m.sampel)
.thenComparing(m -> m.nama));
System.out.printf("%-20s %8s %8s %12s%n", "method", "sampel", "unit", "porsi");
for (Metode m : semua) {
double porsi = 100.0 * m.sampel * INTERVAL_SAMPLING / totalUnit;
System.out.printf("%-20s %8d %8d %11.1f%%%n",
m.nama, m.sampel, m.unitKerja, porsi);
}
System.out.println();
semua.sort(Comparator.comparingInt((Metode m) -> -m.inklusif)
.thenComparing(m -> m.nama));
System.out.printf("%-20s %10s%n", "method", "inklusif");
for (Metode m : semua) {
System.out.printf("%-20s %10d%n", m.nama, m.inklusif);
}
System.out.println();
System.out.println("Sasaran optimasi (porsi eksklusif >= 20%):");
for (Metode m : semua) {
if (100.0 * m.sampel * INTERVAL_SAMPLING / totalUnit >= 20) {
System.out.println(" - " + m.nama + ": periksa algoritma dan sumber daya");
}
}
}
static Metode bangunPanggilan() {
return new Metode("main", 2).panggil(
new Metode("prosesPesanan", 3)
.panggil(new Metode("validasiPesanan", 4))
.panggil(new Metode("hitungTotal", 10)
.panggil(new Metode("diskonBertingkat", 30)))
.panggil(new Metode("simpanKeBasisData", 6)
.panggil(new Metode("koneksiBaru", 25))));
}
static void jalankan(Metode m) {
for (int u = 0; u < m.unitKerja; u++) {
unitBerjalan++;
if (unitBerjalan % INTERVAL_SAMPLING == 0) {
m.sampel++;
}
}
for (Metode a : m.anak) {
jalankan(a);
}
}
static int hitungInklusif(Metode m) {
int total = m.unitKerja;
for (Metode a : m.anak) {
total += hitungInklusif(a);
}
m.inklusif = total;
return total;
}
static void kumpulkan(Metode m, List<Metode> wadah) {
wadah.add(m);
for (Metode a : m.anak) {
kumpulkan(a, wadah);
}
}
static void cetakPohon(Metode m, int kedalaman) {
System.out.println(" ".repeat(kedalaman) + m.nama + " (" + m.unitKerja + ")");
for (Metode a : m.anak) {
cetakPohon(a, kedalaman + 1);
}
}
}