Masalah CPU terlihat sebagai angka yang tinggi; masalah memori lebih licik — aplikasi berjalan normal berjam-jam, lalu melambat, lalu berhenti dengan OutOfMemoryError. Memory profiling adalah teknik memotret isi heap lalu membaca peta itu: objek apa yang tinggal, berapa besar, dan — pertanyaan terpentingnya — siapa yang menahannya tetap hidup. Chapter ini memperkenalkan heap dump dan histogram dari jcmd, tiga angka kunci tiap objek (shallow, retained, dominator), dan kalkulator retensi yang menghitung semuanya di atas graf objek model.
Heap Dump dan Histogram #
Dua perintah jcmd memberi potret memori tanpa alat tambahan. Histogram menghitung instance per class, terurut menurun menurut byte:
jcmd <pid> GC.class_histogram num #instances #bytes class name (module)
-------------------------------------------------------
1: 45 80001960 [J ([email protected])
2: 8726 388392 [B ([email protected])
3: 1692 223952 java.lang.Class ([email protected])
4: 8650 207600 java.lang.String ([email protected])Baris [J (array long) pada proses uji kita adalah 40 array dua juta byte yang memang disengaja program — histogram langsung menunjuk penyumbang terbesar. Histogram tidak menunjukkan siapa memegang siapa; untuk itu kita ambil potret penuh dengan jcmd <pid> GC.heap_dump berkas.hprof lalu membukanya di Eclipse MAT atau VisualVM. Untuk tren waktu nyata, event JFR seperti jdk.ObjectAllocationSample mencatat alokasi beserta stack trace-nya selagi terjadi.
Tiga Angka untuk Setiap Objek #
Membaca heap dump menuntut tiga istilah yang sering tertukar:
| Istilah | Arti | Pertanyaan yang dijawab |
|---|---|---|
| Shallow size | ukuran objek itu sendiri | berapa byte objek ini di heap |
| Retained size | ukuran objek + objek yang ikut bebas bila objek ini dilepas | berapa memori sebenarnya tertahan |
| Dominator | pemegang tunggal yang jalurnya wajib dilewati | siapa penjaga gerbang memori ini |
Objek yang dibagikan dua pemilik tidak dibebaskan dengan menghapus salah satu pemiliknya — fakta sederhana ini adalah inti diagnosa kebocoran memori.
Kalkulator Retensi di Atas Kertas #
Program berikut membangun graf objek mini — aplikasi boking dengan cache pengguna, sesi belanja, dan log audit yang berbagi profil — lalu menghitung ketiga angka itu dengan penelusuran graf sungguhan:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Kalkulator shallow size, retained size, dan pohon dominator atas graf
* objek model dengan ukuran byte tetap. Semua angka hasil penelusuran
* graf murni, bukan statistik GC sungguhan.
*/
public class UkurRetensi {
static final Map<String, Simpul> simpul = new LinkedHashMap<>();
static final class Simpul {
final String nama;
final int shallow;
final List<Simpul> keluar = new ArrayList<>();
Simpul(String nama, int shallow) {
this.nama = nama;
this.shallow = shallow;
}
void rujuk(Simpul s) {
keluar.add(s);
}
}
public static void main(String[] args) {
bangunGraf();
Simpul akar = simpul.get("Aplikasi");
int totalHeap = ukuranTerjangkau(akar, null);
System.out.println("== Peta referensi (ukuran model, byte) ==");
for (Simpul s : simpul.values()) {
if (!s.keluar.isEmpty()) {
List<String> tujuan = new ArrayList<>();
for (Simpul k : s.keluar) {
tujuan.add(k.nama);
}
System.out.println(s.nama + " -> " + String.join(", ", tujuan));
}
}
System.out.println("Total heap model = " + totalHeap + " byte");
System.out.println();
System.out.println("== Shallow vs retained ==");
System.out.printf("%-16s %8s %9s%n", "node", "shallow", "retained");
Map<String, Integer> masuk = hitungIndegree();
for (Simpul s : simpul.values()) {
int retained = totalHeap - ukuranTerjangkau(akar, s);
String tanda = masuk.get(s.nama) >= 2 ? " <- dibagikan" : "";
System.out.printf("%-16s %8d %9d%s%n", s.nama, s.shallow, retained, tanda);
}
System.out.println();
System.out.println("== Pohon dominator (retained) ==");
Map<String, Integer> petaRetained = new LinkedHashMap<>();
for (Simpul s : simpul.values()) {
petaRetained.put(s.nama, totalHeap - ukuranTerjangkau(akar, s));
}
Map<String, List<Simpul>> anak = pohonDominator(akar);
cetakPohon(akar, anak, petaRetained, 0);
System.out.println();
System.out.println("== Catatan pembacaan ==");
for (String n : List.of("ProfilBudi", "KeranjangBudi", "CachePengguna")) {
Simpul s = simpul.get(n);
int retained = totalHeap - ukuranTerjangkau(akar, s);
int ikutBebas = retained - s.shallow;
System.out.println(n + ": retained " + retained + " = shallow " + s.shallow
+ " + " + ikutBebas + " milik objek yang ikut bebas");
}
}
static void bangunGraf() {
Simpul aplikasi = buat("Aplikasi", 16);
Simpul konfigurasi = buat("Konfigurasi", 48);
Simpul cache = buat("CachePengguna", 40);
Simpul budi = buat("PenggunaBudi", 32);
Simpul siti = buat("PenggunaSiti", 32);
Simpul profilBudi = buat("ProfilBudi", 56);
Simpul profilSiti = buat("ProfilSiti", 56);
Simpul logAudit = buat("LogAudit", 64);
Simpul sesi = buat("SesiAktif", 24);
Simpul keranjang = buat("KeranjangBudi", 32);
Simpul item1 = buat("ItemBarang-1", 24);
Simpul item2 = buat("ItemBarang-2", 24);
Simpul item3 = buat("ItemBarang-3", 24);
aplikasi.rujuk(konfigurasi);
aplikasi.rujuk(cache);
aplikasi.rujuk(logAudit);
aplikasi.rujuk(sesi);
cache.rujuk(budi);
cache.rujuk(siti);
budi.rujuk(profilBudi);
siti.rujuk(profilSiti);
logAudit.rujuk(profilBudi); // ProfilBudi dibagikan dua pemilik
sesi.rujuk(keranjang);
keranjang.rujuk(budi); // PenggunaBudi punya dua jalur dari akar
keranjang.rujuk(item1);
keranjang.rujuk(item2);
keranjang.rujuk(item3);
}
static Simpul buat(String nama, int shallow) {
Simpul s = new Simpul(nama, shallow);
simpul.put(nama, s);
return s;
}
// Jumlah byte objek yang masih terjangkau dari akar setelah 'dihapus' dilepas
static int ukuranTerjangkau(Simpul akar, Simpul dihapus) {
if (dihapus == akar) {
return 0; // melepas akar = seluruh heap bebas
}
Map<String, Boolean> dikunjungi = new LinkedHashMap<>();
Deque<Simpul> tumpukan = new ArrayDeque<>();
tumpukan.push(akar);
dikunjungi.put(akar.nama, true);
int total = 0;
while (!tumpukan.isEmpty()) {
Simpul s = tumpukan.pop();
total += s.shallow;
for (Simpul k : s.keluar) {
if (k != dihapus && !dikunjungi.containsKey(k.nama)) {
dikunjungi.put(k.nama, true);
tumpukan.push(k);
}
}
}
return total;
}
static Map<String, Integer> hitungIndegree() {
Map<String, Integer> masuk = new LinkedHashMap<>();
for (Simpul s : simpul.values()) {
masuk.putIfAbsent(s.nama, 0);
for (Simpul k : s.keluar) {
masuk.merge(k.nama, 1, Integer::sum);
}
}
return masuk;
}
static Map<String, List<Simpul>> pohonDominator(Simpul akar) {
// Dominator d: melepas d membuat node n tak terjangkau.
// Immediate dominator = dominator terdalam (terdekat ke n).
Map<String, Integer> jarak = new LinkedHashMap<>();
Deque<Simpul> antrean = new ArrayDeque<>();
jarak.put(akar.nama, 0);
antrean.add(akar);
while (!antrean.isEmpty()) {
Simpul s = antrean.poll();
for (Simpul k : s.keluar) {
if (!jarak.containsKey(k.nama)) {
jarak.put(k.nama, jarak.get(s.nama) + 1);
antrean.add(k);
}
}
}
Map<String, List<Simpul>> anak = new LinkedHashMap<>();
for (Simpul n : simpul.values()) {
if (n == akar) {
continue;
}
Simpul idom = akar;
for (Simpul d : simpul.values()) {
if (d == n || d == akar) {
continue;
}
boolean nHilang = ukuranTerjangkauNode(akar, d, n);
if (nHilang && jarak.get(d.nama) > jarak.get(idom.nama)) {
idom = d;
}
}
anak.computeIfAbsent(idom.nama, k -> new ArrayList<>()).add(n);
}
return anak;
}
// Apakah node 'dicari' tak terjangkau dari akar setelah 'dihapus' dilepas?
static boolean ukuranTerjangkauNode(Simpul akar, Simpul dihapus, Simpul dicari) {
Map<String, Boolean> dikunjungi = new LinkedHashMap<>();
Deque<Simpul> tumpukan = new ArrayDeque<>();
tumpukan.push(akar);
dikunjungi.put(akar.nama, true);
while (!tumpukan.isEmpty()) {
Simpul s = tumpukan.pop();
if (s == dicari) {
return false;
}
for (Simpul k : s.keluar) {
if (k != dihapus && !dikunjungi.containsKey(k.nama)) {
dikunjungi.put(k.nama, true);
tumpukan.push(k);
}
}
}
return true;
}
static void cetakPohon(Simpul s, Map<String, List<Simpul>> anak,
Map<String, Integer> retained, int kedalaman) {
System.out.println(" ".repeat(kedalaman) + s.nama + " (" + retained.get(s.nama) + ")");
for (Simpul a : anak.getOrDefault(s.nama, List.of())) {
cetakPohon(a, anak, retained, kedalaman + 1);
}
}
}Output yang Dihasilkan:
== Peta referensi (ukuran model, byte) ==
Aplikasi -> Konfigurasi, CachePengguna, LogAudit, SesiAktif
CachePengguna -> PenggunaBudi, PenggunaSiti
PenggunaBudi -> ProfilBudi
PenggunaSiti -> ProfilSiti
LogAudit -> ProfilBudi
SesiAktif -> KeranjangBudi
KeranjangBudi -> PenggunaBudi, ItemBarang-1, ItemBarang-2, ItemBarang-3
Total heap model = 472 byte
== Shallow vs retained ==
node shallow retained
Aplikasi 16 472
Konfigurasi 48 48
CachePengguna 40 128
PenggunaBudi 32 32 <- dibagikan
PenggunaSiti 32 88
ProfilBudi 56 56 <- dibagikan
ProfilSiti 56 56
LogAudit 64 64
SesiAktif 24 128
KeranjangBudi 32 104
ItemBarang-1 24 24
ItemBarang-2 24 24
ItemBarang-3 24 24
== Pohon dominator (retained) ==
Aplikasi (472)
Konfigurasi (48)
CachePengguna (128)
PenggunaSiti (88)
ProfilSiti (56)
PenggunaBudi (32)
ProfilBudi (56)
LogAudit (64)
SesiAktif (128)
KeranjangBudi (104)
ItemBarang-1 (24)
ItemBarang-2 (24)
ItemBarang-3 (24)
== Catatan pembacaan ==
ProfilBudi: retained 56 = shallow 56 + 0 milik objek yang ikut bebas
KeranjangBudi: retained 104 = shallow 32 + 72 milik objek yang ikut bebas
CachePengguna: retained 128 = shallow 40 + 88 milik objek yang ikut bebasMari kita telaah bagian-bagian kuncinya:
- Baris 91-104 (
bangunGraf): dua keputusan desain sengaja ditanam —logAudit.rujuk(profilBudi)membuat ProfilBudi dipegang dua pemilik, dankeranjang.rujuk(budi)memberi PenggunaBudi dua jalur dari akar. - Baris 113-135 (
ukuranTerjangkau): fungsi inti kalkulator. Untuk mengukur retained sebuah simpul, simpul itu “dilepas”, penelusuran DFS dijalankan ulang dari akar, dan seluruh byte yang gagal terjangkau dijumlahkan — definisi retained yang sesungguhnya, bukan sekadar ukuran subtree. - Baris 50-56 (tabel): hasil menunjukkan
ProfilBudidengan shallow = retained = 56. Karena LogAudit masih memegang profil itu, menghapus PenggunaBudi tidak membebaskan satu byte pun dari profilnya. - Baris 147-181 (
pohonDominator): immediate dominator dicari dengan pengujian menyeluruh — setiap kandidat diuji dengan “apakah melepas kandidat ini memutus jalur ke simpul n?” — lalu dominator terdalam menang. Hasilnya, PenggunaBudi dan ProfilBudi menggantung langsung di bawah Aplikasi: objek berbagi pemilik “mengapung” ke atas pohon dominator.
Membaca Dominator untuk Menemukan Kebocoran #
Pohon dominator adalah peta tersangka. CachePengguna menahan 128 byte dari total 472 — hampir seperempat heap — sehingga pantas diperiksa kebijakan pembersihannya; sementara LogAudit yang tampak besar (64 byte shallow) ternyata hanya menahan dirinya sendiri. Pola kebocoran klasik terbaca sebagai cabang dominator yang retained-nya tumbuh terus: cache tanpa batas, daftar riwayat tak pernah dipangkas, listener yang tak pernah dilepas pasangannya. Tanda-tanda dan polanya dibedah pada Chapter Memory Leaks di section JVM dan Memory Management.
Retensi menjelaskan objek yang sudah ada; pertanyaan berikutnya adalah dari mana semua objek itu datang. Chapter berikutnya menyisir sumber alokasi — dan bagaimana JIT kadang menghapus alokasi yang tidak pernah lolos dari method-nya.
Referensi #
- Oracle: jcmd — JDK 25 Tool Reference
- Oracle: package jdk.jfr (Java SE 25)
- Oracle: Troubleshooting — Diagnostic Tools
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
UkurRetensi.java
Unduh berkasimport java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Kalkulator shallow size, retained size, dan pohon dominator atas graf
* objek model dengan ukuran byte tetap. Semua angka hasil penelusuran
* graf murni, bukan statistik GC sungguhan.
*/
public class UkurRetensi {
static final Map<String, Simpul> simpul = new LinkedHashMap<>();
static final class Simpul {
final String nama;
final int shallow;
final List<Simpul> keluar = new ArrayList<>();
Simpul(String nama, int shallow) {
this.nama = nama;
this.shallow = shallow;
}
void rujuk(Simpul s) {
keluar.add(s);
}
}
public static void main(String[] args) {
bangunGraf();
Simpul akar = simpul.get("Aplikasi");
int totalHeap = ukuranTerjangkau(akar, null);
System.out.println("== Peta referensi (ukuran model, byte) ==");
for (Simpul s : simpul.values()) {
if (!s.keluar.isEmpty()) {
List<String> tujuan = new ArrayList<>();
for (Simpul k : s.keluar) {
tujuan.add(k.nama);
}
System.out.println(s.nama + " -> " + String.join(", ", tujuan));
}
}
System.out.println("Total heap model = " + totalHeap + " byte");
System.out.println();
System.out.println("== Shallow vs retained ==");
System.out.printf("%-16s %8s %9s%n", "node", "shallow", "retained");
Map<String, Integer> masuk = hitungIndegree();
for (Simpul s : simpul.values()) {
int retained = totalHeap - ukuranTerjangkau(akar, s);
String tanda = masuk.get(s.nama) >= 2 ? " <- dibagikan" : "";
System.out.printf("%-16s %8d %9d%s%n", s.nama, s.shallow, retained, tanda);
}
System.out.println();
System.out.println("== Pohon dominator (retained) ==");
Map<String, Integer> petaRetained = new LinkedHashMap<>();
for (Simpul s : simpul.values()) {
petaRetained.put(s.nama, totalHeap - ukuranTerjangkau(akar, s));
}
Map<String, List<Simpul>> anak = pohonDominator(akar);
cetakPohon(akar, anak, petaRetained, 0);
System.out.println();
System.out.println("== Catatan pembacaan ==");
for (String n : List.of("ProfilBudi", "KeranjangBudi", "CachePengguna")) {
Simpul s = simpul.get(n);
int retained = totalHeap - ukuranTerjangkau(akar, s);
int ikutBebas = retained - s.shallow;
System.out.println(n + ": retained " + retained + " = shallow " + s.shallow
+ " + " + ikutBebas + " milik objek yang ikut bebas");
}
}
static void bangunGraf() {
Simpul aplikasi = buat("Aplikasi", 16);
Simpul konfigurasi = buat("Konfigurasi", 48);
Simpul cache = buat("CachePengguna", 40);
Simpul budi = buat("PenggunaBudi", 32);
Simpul siti = buat("PenggunaSiti", 32);
Simpul profilBudi = buat("ProfilBudi", 56);
Simpul profilSiti = buat("ProfilSiti", 56);
Simpul logAudit = buat("LogAudit", 64);
Simpul sesi = buat("SesiAktif", 24);
Simpul keranjang = buat("KeranjangBudi", 32);
Simpul item1 = buat("ItemBarang-1", 24);
Simpul item2 = buat("ItemBarang-2", 24);
Simpul item3 = buat("ItemBarang-3", 24);
aplikasi.rujuk(konfigurasi);
aplikasi.rujuk(cache);
aplikasi.rujuk(logAudit);
aplikasi.rujuk(sesi);
cache.rujuk(budi);
cache.rujuk(siti);
budi.rujuk(profilBudi);
siti.rujuk(profilSiti);
logAudit.rujuk(profilBudi); // ProfilBudi dibagikan dua pemilik
sesi.rujuk(keranjang);
keranjang.rujuk(budi); // PenggunaBudi punya dua jalur dari akar
keranjang.rujuk(item1);
keranjang.rujuk(item2);
keranjang.rujuk(item3);
}
static Simpul buat(String nama, int shallow) {
Simpul s = new Simpul(nama, shallow);
simpul.put(nama, s);
return s;
}
// Jumlah byte objek yang masih terjangkau dari akar setelah 'dihapus' dilepas
static int ukuranTerjangkau(Simpul akar, Simpul dihapus) {
if (dihapus == akar) {
return 0; // melepas akar = seluruh heap bebas
}
Map<String, Boolean> dikunjungi = new LinkedHashMap<>();
Deque<Simpul> tumpukan = new ArrayDeque<>();
tumpukan.push(akar);
dikunjungi.put(akar.nama, true);
int total = 0;
while (!tumpukan.isEmpty()) {
Simpul s = tumpukan.pop();
total += s.shallow;
for (Simpul k : s.keluar) {
if (k != dihapus && !dikunjungi.containsKey(k.nama)) {
dikunjungi.put(k.nama, true);
tumpukan.push(k);
}
}
}
return total;
}
static Map<String, Integer> hitungIndegree() {
Map<String, Integer> masuk = new LinkedHashMap<>();
for (Simpul s : simpul.values()) {
masuk.putIfAbsent(s.nama, 0);
for (Simpul k : s.keluar) {
masuk.merge(k.nama, 1, Integer::sum);
}
}
return masuk;
}
static Map<String, List<Simpul>> pohonDominator(Simpul akar) {
// Dominator d: melepas d membuat node n tak terjangkau.
// Immediate dominator = dominator terdalam (terdekat ke n).
Map<String, Integer> jarak = new LinkedHashMap<>();
Deque<Simpul> antrean = new ArrayDeque<>();
jarak.put(akar.nama, 0);
antrean.add(akar);
while (!antrean.isEmpty()) {
Simpul s = antrean.poll();
for (Simpul k : s.keluar) {
if (!jarak.containsKey(k.nama)) {
jarak.put(k.nama, jarak.get(s.nama) + 1);
antrean.add(k);
}
}
}
Map<String, List<Simpul>> anak = new LinkedHashMap<>();
for (Simpul n : simpul.values()) {
if (n == akar) {
continue;
}
Simpul idom = akar;
for (Simpul d : simpul.values()) {
if (d == n || d == akar) {
continue;
}
boolean nHilang = ukuranTerjangkauNode(akar, d, n);
if (nHilang && jarak.get(d.nama) > jarak.get(idom.nama)) {
idom = d;
}
}
anak.computeIfAbsent(idom.nama, k -> new ArrayList<>()).add(n);
}
return anak;
}
// Apakah node 'dicari' tak terjangkau dari akar setelah 'dihapus' dilepas?
static boolean ukuranTerjangkauNode(Simpul akar, Simpul dihapus, Simpul dicari) {
Map<String, Boolean> dikunjungi = new LinkedHashMap<>();
Deque<Simpul> tumpukan = new ArrayDeque<>();
tumpukan.push(akar);
dikunjungi.put(akar.nama, true);
while (!tumpukan.isEmpty()) {
Simpul s = tumpukan.pop();
if (s == dicari) {
return false;
}
for (Simpul k : s.keluar) {
if (k != dihapus && !dikunjungi.containsKey(k.nama)) {
dikunjungi.put(k.nama, true);
tumpukan.push(k);
}
}
}
return true;
}
static void cetakPohon(Simpul s, Map<String, List<Simpul>> anak,
Map<String, Integer> retained, int kedalaman) {
System.out.println(" ".repeat(kedalaman) + s.nama + " (" + retained.get(s.nama) + ")");
for (Simpul a : anak.getOrDefault(s.nama, List.of())) {
cetakPohon(a, anak, retained, kedalaman + 1);
}
}
}