Aplikasi Java dan Kotlin mengelola memori melalui heap yang dikumpulkan sampah. Saat objek tidak dapat dijangkau lagi, pengumpul sampah (GC) pada akhirnya akan merebut kembali ruangnya. Kebocoran memori terjadi saat objek yang tidak lagi diperlukan masih dipertahankan oleh "GC root", sehingga mencegahnya diklaim kembali.
Konsep inti
Akar GC
GC Root adalah jenis objek khusus yang dianggap selalu dapat dijangkau oleh pengumpul sampah. Contohnya mencakup:
- Rangkaian pesan aktif (dan objek yang dirujuk dari frame stack Java yang sedang dieksekusi).
- Class dengan metode yang berjalan secara aktif.
- Referensi JNI (referensi global atau lokal yang dipegang oleh kode native).
Jalur ke root GC
Selama ada rantai referensi dari Root GC ke objek, objek tersebut "dapat dijangkau" dan tidak dapat dikumpulkan sampah. Rantai ini disebut Path to GC Root. Untuk memperbaiki kebocoran memori, Anda harus mengidentifikasi dan memutus rantai ini.

Pohon dominator
Meskipun jalur ke root GC memberi tahu Anda mengapa suatu objek tetap aktif, jalur tersebut tidak memberi tahu Anda berapa banyak memori yang akan diklaim kembali jika referensi tersebut rusak. Untuk itu, kita menggunakan Pohon Dominator.
Objek A dikatakan mendominasi objek B jika setiap jalur dari root GC mana pun ke B harus melewati A. Jika A mendominasi B, maka mengklaim ulang A juga akan menjamin bahwa B dapat diklaim ulang, karena tidak ada jalur lain dari root ke B.
Diagram berikut menunjukkan grafik objek dan pohon dominator yang sesuai. Perhatikan bagaimana objek D dijangkau oleh A dan B dalam grafik, sehingga A maupun B tidak mendominasi D; sebagai gantinya, GC Root adalah dominator terdekatnya.

Mendapatkan heap dump Java
Heap dump adalah snapshot semua objek di heap Java pada titik waktu tertentu.
Menggunakan ADB
Untuk merekam heap dump dari proses yang sedang berjalan, Anda dapat meneruskan nama paket
langsung ke am dumpheap. Untuk menjalankan perintah ini, Anda harus membangun aplikasi dengan
<profileable android:shell="true"/> atau <debuggable>.
# 1. Trigger the dump (the command takes a moment to complete):
adb shell am dumpheap -g -b png com.android.memorylab /data/local/tmp/heap.hprof
# 2. Pull the file to your development machine:
adb pull /data/local/tmp/heap.hprof .
Menggunakan Perfetto
Perfetto juga dapat merekam dump heap Java sebagai bagian dari rekaman aktivitas di seluruh sistem dengan
mengaktifkan sumber data android.java_hprof dalam konfigurasi Perfetto Anda. Hal ini berguna untuk menghubungkan status heap dengan peristiwa sistem lainnya.
Untuk merekam heap dump aplikasi MemoryLab menggunakan Perfetto, Anda dapat menggunakan perintah berikut:
# Create a temp file for the configuration
cat > /tmp/java_heap.pbtx <<EOF
data_sources: {
config {
name: "android.java_hprof"
java_hprof_config {
process_cmdline: "com.android.memorylab"
}
}
}
EOF
# Run trace command referencing the file
external/perfetto/tools/record_android_trace -o java_heap.perfetto-trace \
-t 10s -c /tmp/java_heap.pbtx
Lihat: Dokumen heap dump Java di Perfetto.
Menganalisis dengan AHAT
AHAT (Android Heap Analysis Tool) adalah alat yang direkomendasikan untuk melihat file .hprof
di browser web.
Memulai AHAT
Jika Anda telah menginstal ahat di jalur Anda, luncurkan dengan:
ahat heap.hprof
Atau jalankan jar mandiri:
java -jar ahat.jar heap.hprof
Kemudian, buka browser Anda ke http://localhost:7100.
Untuk mengetahui detail tentang cara mendapatkan atau membuat AHAT, lihat repositori sumber AHAT.
Alur kerja analisis utama
Menemukan kebocoran
Cari kelas Aktivitas Anda (MainActivity) di tampilan Alokasi.

Klik kelas untuk menemukan semua Instance.
Klik instance
MainActivity untuk memeriksanya.

Di tampilan instance, Anda dapat menemukan Sample Path from GC Root, yang menunjukkan rantai referensi yang mencegah objek dikumpulkan sampah, dan Object Size, yang menunjukkan jumlah memori yang dipertahankan oleh instance tertentu ini.

Menganalisis bitmap
AHAT memiliki dukungan khusus untuk melihat objek android.graphics.Bitmap, yang sering kali menggunakan memori dalam jumlah besar. Klik instance Bitmap untuk melihat pratinjau kontennya yang dirender.

Halaman kebocoran aktivitas
AHAT memiliki tampilan khusus untuk mengidentifikasi Aktivitas yang bocor, yang merupakan salah satu kebocoran memori paling umum dan berdampak di Android.
- Tindakan: Di MemoryLab, ketuk Bocorkan Aktivitas. Tindakan ini meluncurkan
LeakedActivityyang sengaja membocorkan dirinya sendiri. - Dump: Ambil heap dump.
- Analisis: Klik Kebocoran Aktivitas di sidebar AHAT.
- Verifikasi: AHAT akan mencantumkan
com.android.memorylab.LeakedActivitysebagai bocor karena kolommDestroyed-nya benar (menunjukkan siklus proses Aktivitas telah berakhir) tetapi masih dapat dijangkau dari root GC.

Membedakan heap dump
Membandingkan dua dump heap adalah salah satu cara paling efektif untuk mengidentifikasi masalah memori. Dengan membandingkan dump dasar "bersih" dengan dump yang diambil setelah melakukan beberapa tindakan, Anda dapat langsung melihat objek mana yang telah terakumulasi.
Latihan: Mengidentifikasi Kebocoran melalui Perbedaan
Dasar: Luncurkan MemoryLab dan ambil heap dump dasar:
adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof adb pull /data/local/tmp/base.hprof .Tindakan: Ketuk Allocate Java Memory(10MB) beberapa kali di aplikasi.
Final: Ambil heap dump kedua:
adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof adb pull /data/local/tmp/leaked.hprof .Bandingkan: Mulai AHAT dengan dump kedua sebagai yang utama dan yang pertama sebagai dasar:
java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprofRingkasan Analisis: Halaman Ringkasan kini menyertakan kolom Δ (Delta). Anda akan melihat delta positif yang besar untuk heap
app, yang menunjukkan pertumbuhan memori yang signifikan.

- Lihat Perincian: Klik berakar di menu. Halaman ini menampilkan objek yang dapat dijangkau dari root GC, yang diurutkan berdasarkan ukuran yang dipertahankan. Anda akan melihat
MainActivitydi bagian atas dengan delta positif yang besar.

Merekam stack trace alokasi
Meskipun Sample Path from GC Root memberi tahu Anda mengapa suatu objek masih aktif, objek tersebut tidak memberi tahu Anda cara objek tersebut dibuat. Stack trace alokasi memberikan baris kode persis yang mengalokasikan objek.
Konsep & Trade-off: Merekam stack trace setiap alokasi memerlukan komputasi yang mahal dan menggunakan memori yang signifikan. Dalam aplikasi produksi yang besar, hal ini dapat membuat aplikasi hampir tidak dapat digunakan. Namun, MemoryLab adalah aplikasi yang cukup kecil sehingga kita dapat mengaktifkan pelacakan ini dengan aman untuk menentukan sumber alokasi.
Latihan: Mengidentifikasi sumber Byte Array
Mulai dengan Pelacakan: Hentikan MemoryLab secara paksa dan mulai ulang dengan tanda
--track-allocation. Tingkatkan kedalaman stack default untuk mendapatkan lebih banyak konteks.# Increase the allocation tracker's stack depth (requires a process restart) adb shell setprop dalvik.vm.allocTrackerMaxStack 16 adb shell am force-stop com.android.memorylab adb shell am start --track-allocation -n com.android.memorylab/.MainActivityTindakan: Ketuk Allocate Java Memory(10MB) beberapa kali.
Dump: Ambil dan tarik heap dump.
Analisis: Buka dump di AHAT. Buka instance
byte[]berukuran besar. (misalnya, periksaMainActivity→mJavaAllocations(ArrayList) →elementData(Object[]) → elemen array[0]).Verifikasi: Di tampilan instance, lihat bagian Situs Alokasi. Langkah ini akan menampilkan stack trace lengkap yang mengarah ke
MainActivity.allocateJava.

Mencari string duplikat dan pembengkakan hidrasi
Meskipun aplikasi tidak memiliki kebocoran GC-root klasik, heap Java live-nya dapat
membengkak karena ribuan instance java.lang.String duplikat yang dibuat selama
deserialisasi JSON, Protobuf, Cursor, atau database Room. Kunci berulang, string status, label kategori, atau URL sering kali dialokasikan ulang pada setiap respons jaringan atau kueri database. Di seluruh aplikasi feed, pesan, dan konten besar, string duplikat secara rutin menyumbang 30% hingga 60% memori aktif String.
Untuk memeriksa string duplikat di AHAT:
- Buka halaman Alokasi dan filter menurut
java.lang.String. - Saat membandingkan dua dump heap dengan
--baseline, periksa apakah jumlah instancejava.lang.Stringdan total byte bertambah secara tidak proporsional setelah melakukan hidrasi feed atau memuat cache lokal. - Jelajahi tabel instance
java.lang.String(diurutkan berdasarkan ukuran atau nilai) untuk menemukan nilai string identik yang dipertahankan di beberapa objek model dalam memori.
- Solusi: Hindari memanggil
String.intern()secara sembarangan pada input pengguna atau jaringan arbitrer, karena tabel intern runtime bersifat global dan dapat memperkenalkan pertentangan kunci atau mempertahankan string lebih lama dari yang diperlukan. Sebagai gantinya, hapus duplikat string domain frekuensi tinggi selama deserialisasi menggunakan cache penghapusan duplikat yang tercakup dan terbatas (sepertiLruCache<String, String>di dalam parser atau adaptor Anda), atau representasikan kumpulan nilai tetap sebagai enum atau konstanta bilangan bulat.
Menganalisis dinamika memori Java (profil gabungan)
Untuk mendapatkan gambaran lengkap tentang perilaku memori aplikasi, Anda dapat menggabungkan penghitung memori, aktivitas thread, dan pembuatan profil alokasi berbasis callstack ke dalam rekaman aktivitas Perfetto tunggal. Hal ini memungkinkan Anda mengorelasikan metrik memori di seluruh sistem (seperti RSS dan ukuran heap) dengan eksekusi kode dan situs alokasi tertentu.
Kita akan menggunakan konfigurasi gabungan yang memungkinkan:
- Penghitung Memori (
linux.process_stats): Meminta RSS dan metrik memori lainnya. - ATrace (kategori
dalvik,memory,sched): Merekam status thread dan peristiwa GC. - Heapprofd (
android.heapprofd): Menargetkan heapcom.android.art(Java) danlibc.malloc(native) dengan dump berkelanjutan setiap 5 detik.
Latihan: analisis memori gabungan
Dalam latihan ini, kita akan menjalankan aplikasi MemoryLab dan melakukan serangkaian operasi memori untuk mengamati berbagai pola dalam rekaman aktivitas:
- Dasar pengukuran: Status tidak ada aktivitas.
- Churn Java: Alokasi sementara yang langsung dikumpulkan sampah.
- Alokasi Java Persisten: Mengalokasikan objek Java yang tetap berada dalam memori.
- Alokasi Bitmap: Mengalokasikan aset grafis besar (yang berada di heap/memori grafis native).
- Pengambilan kembali: Membebaskan semua resource yang dialokasikan.
1. Luncurkan dan siapkan
Hentikan paksa dan mulai ulang aplikasi untuk memastikan status yang bersih:
adb shell am force-stop com.android.memorylab adb shell am start -W -n com.android.memorylab/.MainActivity
2. Mulai pelacakan dan urutan pemicu
Kita akan memulai rekaman aktivitas selama 40 detik dan memicu peristiwa memori menggunakan perintah am
broadcast.
Mulai rekaman aktivitas:
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/java_memory.perfetto-trace <<EOF buffers: { size_kb: 131072 fill_policy: RING_BUFFER } data_sources: { config { name: "linux.process_stats" target_buffer: 0 process_stats_config { scan_all_processes_on_start: true proc_stats_poll_ms: 100 } } } data_sources: { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "task/task_newtask" ftrace_events: "task/task_rename" ftrace_events: "ftrace/print" atrace_categories: "dalvik" atrace_categories: "am" atrace_categories: "res" atrace_categories: "memory" atrace_categories: "sched" atrace_apps: "com.android.memorylab" } } } data_sources: { config { name: "android.heapprofd" target_buffer: 0 heapprofd_config { sampling_interval_bytes: 4096 process_cmdline: "com.android.memorylab" heaps: "libc.malloc" heaps: "com.android.art" shmem_size_bytes: 8388608 block_client: true continuous_dump_config { dump_phase_ms: 1000 dump_interval_ms: 5000 } } } } duration_ms: 40000 EOFPicu urutan (jalankan perintah ini di terminal host saat pelacakan sedang berjalan, dengan memperhatikan waktu yang disarankan):
# Wait ~5s for trace initialization, then start Java churn: adb shell am broadcast -a com.android.memorylab.CHURN_JAVA # Wait ~10s (at 15s mark), allocate 10MB of persistent Java memory: adb shell am broadcast -a com.android.memorylab.ALLOC_JAVA # Wait ~5s (at 20s mark), allocate 20MB of Bitmaps (native/graphics): adb shell am broadcast -a com.android.memorylab.LEAK_BITMAP # Wait ~10s (at 30s mark), free everything: adb shell am broadcast -a com.android.memorylab.FREE_ALLAlternatif (Alat CLI): Anda juga dapat memulai pembuatan profil menggunakan skrip
heap_profilesecara langsung, yang menargetkan heap Java dan native dengan dump berkelanjutan:external/perfetto/tools/heap_profile -n com.android.memorylab \ --heaps com.android.art,libc.malloc \ -c 5000 \ -d 40000 \ -o java_memory_profile
3. Menganalisis rekaman aktivitas gabungan
Buka java_memory.perfetto-trace yang dikumpulkan di UI Perfetto.
Jalur utama di Perfetto
Sebelum menganalisis linimasa, temukan jalur penting berikut untuk proses com.android.memorylab:
mem.rss.anon(RSS Anonim): Ditemukan di bagian Memori proses. Jalur ini mengukur memori fisik (RAM) yang dialokasikan ke proses oleh OS. Objek ini merepresentasikan jejak memori yang sebenarnya.Heap size (KB): Juga di bagian Memori. Ini adalah penghitung khusus Dalvik/ART yang merepresentasikan ruang alamat virtual yang dicadangkan untuk heap Java. Nilai ini mencerminkan batas heap internal VM, yang berubah-ubah saat objek dialokasikan dan GC berjalan.HeapTaskDaemon: Ditemukan dalam daftar thread di bawah proses. Ini adalah thread latar belakang tempat Garbage Collector ART melakukan sebagian besar pekerjaannya. Aktivitas di sini menunjukkan proses GC yang aktif.- Dump Alokasi Berkelanjutan (heapprofd): Ditampilkan sebagai irisan berwarna di sepanjang linimasa atas. Setiap irisan mewakili durasi dalam waktu. Dengan mengklik satu irisan atau memilih rentang waktu, Anda dapat memeriksa Flamegraph (di panel bawah) untuk com.android.art (alokasi Java) atau libc.malloc (alokasi Native) untuk melihat apa yang dialokasikan selama periode tersebut.
Analisis fase kronologis
Mari kita tinjau rekaman aktivitas secara kronologis untuk melihat cara interaksi jalur ini selama setiap fase latihan.
Fase 1: dasar (0 dtk - 5 dtk)
- Apa yang terjadi: Aplikasi tidak ada aktivitas, menunggu perintah.
- Status Pelacakan:
mem.rss.anon: Garis datar di garis dasar (biasanya sekitar 60-80 MB, bergantung pada perangkat).Heap size (KB): Garis datar, cocok dengan alokasi heap Java awal.HeapTaskDaemon: Tidak ada aktivitas (tidak ada irisan yang menampilkan eksekusi).- Dump Alokasi: Menampilkan alokasi dasar minimum.

Tahap 2: Perubahan alokasi Java (5 detik - 15 detik)
- Apa yang terjadi:
AllocationChurnThreaddimulai, berulang kali mengalokasikan array 1 MB dan membuangnya. - Status Pelacakan:
Heap size (KB): Menampilkan pola gergaji yang cepat. Ukuran heap meningkat saat alokasi terakumulasi dan menurun tajam saat GC berjalan.HeapTaskDaemon: Menunjukkan aktivitas yang hampir konstan, dengan slice eksekusi yang selaras sempurna dengan penurunan pada gigi gergajiHeap size.mem.rss.anon: Melacak aktivitas heap Java.- Heap Dump Alokasi Java: Memilih slice di jalur ini akan menampilkan alokasi heap com.android.art.
Contoh alokasi mengungkapkan AllocationChurnThread sebagai pengalokasi utama,
dengan semua alokasi berbagi callstack yang sama yang mengarah ke lambda di dalam
MainActivity.java.

Fase 3: alokasi Java persisten (15-20 detik)
- Yang terjadi: Kita mengalokasikan objek Java sebesar 10 MB dan menyimpan referensinya
di
mJavaAllocations. - Status Pelacakan:
Heap size (KB): Baseline langkah sawtooth naik sekitar 10 MB.mem.rss.anon: Meningkat sekitar 10 MB, karena OS harus mendukung alokasi persisten ini dengan halaman fisik baru.- Dump Alokasi Heap Java: Memilih slice di jalur ini akan menampilkan alokasi heap com.android.art.
- Dump Alokasi (Flamegraph): Memeriksa heap com.android.art
untuk dump yang diambil di jendela ini menunjukkan jalur alokasi baru dari
MainActivity.allocateJavayang berkontribusi pada ukuran yang dipertahankan.
Pilih sampel alokasi yang mencakup durasi yang tumpang-tindih dengan peningkatan 10 MB untuk alokasi persisten. Anda akan melihat callstack alokasi yang berbeda
ke dua situs yang berbeda, satu bertanggung jawab atas churn alokasi singkat yang sama
yang kita lihat sebelumnya, dan yang lainnya untuk alokasi jangka panjang yang baru.

Fase 4: alokasi bitmap (20-30 detik)
- Yang terjadi: Kita juga mengalokasikan Bitmap sebesar 20 MB.
- Status Pelacakan:
Heap size (KB): Sama seperti sebelumnya.mem.rss.anon: Menunjukkan peningkatan signifikan sekitar 20 MB, yang sesuai dengan alokasi native untuk data piksel Bitmap.- Dump Alokasi (Flamegraph): Kali ini, fokus pada slice untuk heap libc.malloc (Native).
Callstack alokasi native
mengungkapkan alokasi Bitmap yang berasal dari library grafis native. Ini adalah kasus penggunaan yang baik untuk pelacakan alokasi native, karena Anda
tidak akan melihat alokasi Bitmap ini di heap Java.

Fase 5: reklamasi (30-40 detik)
- Yang terjadi: Kami memicu
FREE_ALL, menghapus referensi ke semua alokasi dan bitmap Java persisten, diikuti denganSystem.gc()eksplisit. - Status Pelacakan:
Heap size (KB): Turun kembali ke tingkat dasar.mem.rss.anon: Turun kembali, menunjukkan OS merebut kembali halaman fisik.HeapTaskDaemon: Menampilkan aktivitas terakhir saat memproses pembersihan sampah memori.

Memantau OOM historis (ApplicationExitInfo)
Mencatat LMK saat terjadi sangat bagus untuk proses debug aktif, tetapi untuk telemetri lapangan, Anda dapat menggunakan ApplicationExitInfo API. Hal ini memungkinkan aplikasi Anda
mengetahui alasan penghentiannya pada sesi sebelumnya.
ActivityManager am = getSystemService(ActivityManager.class);
List<ApplicationExitInfo> exitReasons = am.getHistoricalProcessExitReasons(null, 0, 1);
if (!exitReasons.isEmpty()) {
ApplicationExitInfo info = exitReasons.get(0);
if (info.getReason() == ApplicationExitInfo.REASON_LOW_MEMORY) {
// App was killed by the system Low Memory Killer
}
}
Praktik terbaik
- Dasar Pengukuran Pertama: Selalu ambil dump heap "dasar pengukuran" setelah aplikasi diinisialisasi, tetapi sebelum melakukan tindakan yang Anda uji.
- Menggunakan Halaman Kebocoran Aktivitas AHAT: AHAT menyertakan halaman Kebocoran Aktivitas khusus yang secara otomatis mengidentifikasi instance Aktivitas yang telah dihancurkan, tetapi masih disimpan dalam memori. Cara ini sering kali merupakan cara tercepat untuk menemukan kebocoran umum.
- Periksa Jalur ke GC Roots: Untuk setiap objek yang bocor, gunakan tampilan Jalur dari Root di AHAT untuk memahami secara persis referensi mana yang membuatnya tetap aktif (misalnya, kolom statis, thread yang berjalan lama, atau pemroses yang terdaftar).