Menganalisis memori Java

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.

Jalur ke Root GC

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.

Hierarki Dominator

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.

Tampilan AHAT yang menampilkan instance

Klik kelas untuk menemukan semua Instance.

Tampilan AHAT yang menampilkan instance MainActivity Klik instance MainActivity untuk memeriksanya.

AHAT menampilkan detail instance

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.

Jalur Sampel AHAT dari Root GC dan Ukuran Objek

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.

Pratinjau Bitmap AHAT

Halaman kebocoran aktivitas

AHAT memiliki tampilan khusus untuk mengidentifikasi Aktivitas yang bocor, yang merupakan salah satu kebocoran memori paling umum dan berdampak di Android.

  1. Tindakan: Di MemoryLab, ketuk Bocorkan Aktivitas. Tindakan ini meluncurkan LeakedActivity yang sengaja membocorkan dirinya sendiri.
  2. Dump: Ambil heap dump.
  3. Analisis: Klik Kebocoran Aktivitas di sidebar AHAT.
  4. Verifikasi: AHAT akan mencantumkan com.android.memorylab.LeakedActivity sebagai bocor karena kolom mDestroyed-nya benar (menunjukkan siklus proses Aktivitas telah berakhir) tetapi masih dapat dijangkau dari root GC.

Halaman Kebocoran Aktivitas AHAT

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

  1. 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 .
    
  2. Tindakan: Ketuk Allocate Java Memory(10MB) beberapa kali di aplikasi.

  3. Final: Ambil heap dump kedua:

    adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof
    adb pull /data/local/tmp/leaked.hprof .
    
  4. 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.hprof
    
  5. Ringkasan Analisis: Halaman Ringkasan kini menyertakan kolom Δ (Delta). Anda akan melihat delta positif yang besar untuk heap app, yang menunjukkan pertumbuhan memori yang signifikan.

Ringkasan AHAT dengan Delta

  1. 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 MainActivity di bagian atas dengan delta positif yang besar.

Tampilan yang Di-Root AHAT dengan Delta

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

  1. 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/.MainActivity
    
  2. Tindakan: Ketuk Allocate Java Memory(10MB) beberapa kali.

  3. Dump: Ambil dan tarik heap dump.

  4. Analisis: Buka dump di AHAT. Buka instance byte[] berukuran besar. (misalnya, periksa MainActivity → mJavaAllocations (ArrayList) → elementData (Object[]) → elemen array [0]).

  5. Verifikasi: Di tampilan instance, lihat bagian Situs Alokasi. Langkah ini akan menampilkan stack trace lengkap yang mengarah ke MainActivity.allocateJava.

Situs Alokasi AHAT

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:

  1. Buka halaman Alokasi dan filter menurut java.lang.String.
  2. Saat membandingkan dua dump heap dengan --baseline, periksa apakah jumlah instance java.lang.String dan total byte bertambah secara tidak proporsional setelah melakukan hidrasi feed atau memuat cache lokal.
  3. 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 (seperti LruCache<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 heap com.android.art (Java) dan libc.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:

  1. Dasar pengukuran: Status tidak ada aktivitas.
  2. Churn Java: Alokasi sementara yang langsung dikumpulkan sampah.
  3. Alokasi Java Persisten: Mengalokasikan objek Java yang tetap berada dalam memori.
  4. Alokasi Bitmap: Mengalokasikan aset grafis besar (yang berada di heap/memori grafis native).
  5. Pengambilan kembali: Membebaskan semua resource yang dialokasikan.

1. Luncurkan dan siapkan

  1. 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.

  1. 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
    EOF
    
  2. Picu 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_ALL
    
  3. Alternatif (Alat CLI): Anda juga dapat memulai pembuatan profil menggunakan skrip heap_profile secara 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

UI Perfetto yang menampilkan dasar pengukuran Fase 1

Tahap 2: Perubahan alokasi Java (5 detik - 15 detik)
  • Apa yang terjadi: AllocationChurnThread dimulai, 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 gergaji Heap size.
    • mem.rss.anon: Melacak aktivitas heap Java.
    • Heap Dump Alokasi Java: Memilih slice di jalur ini akan menampilkan alokasi heap com.android.art.

UI Perfetto yang menampilkan churn Fase 2 Contoh alokasi mengungkapkan AllocationChurnThread sebagai pengalokasi utama, dengan semua alokasi berbagi callstack yang sama yang mengarah ke lambda di dalam MainActivity.java.

UI Perfetto yang menampilkan alokasi Java Fase 2

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.allocateJava yang berkontribusi pada ukuran yang dipertahankan.

UI Perfetto yang menampilkan alokasi Java persisten Fase 3 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.

UI Perfetto yang menampilkan alokasi Java Fase 3

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).

UI Perfetto yang menampilkan Alokasi Bitmap Fase 4 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.

UI Perfetto yang menampilkan alokasi Native Fase 4

Fase 5: reklamasi (30-40 detik)
  • Yang terjadi: Kami memicu FREE_ALL, menghapus referensi ke semua alokasi dan bitmap Java persisten, diikuti dengan System.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.

UI Perfetto yang menampilkan Pemulihan Fase 5

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

  1. Dasar Pengukuran Pertama: Selalu ambil dump heap "dasar pengukuran" setelah aplikasi diinisialisasi, tetapi sebelum melakukan tindakan yang Anda uji.
  2. 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.
  3. 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).

← Alat | ↑ Atas | Bitmap →