Java ve Kotlin uygulamaları, çöp toplayıcı tarafından toplanan bir yığın aracılığıyla belleği yönetir. Nesnelere artık ulaşılamadığında çöp toplayıcı (GC) sonunda alanlarını geri alır. Bellek sızıntıları, artık ihtiyaç duyulmayan nesneler "GC kökleri" tarafından tutulmaya devam ettiğinde ve bu nedenle geri alınamadığında meydana gelir.
Temel kavramlar
GC kökleri
GC kökü, çöp toplayıcının her zaman erişilebilir olarak değerlendirdiği özel bir nesne türüdür. Örnekler:
- Etkin iş parçacıkları (ve şu anda yürütülen Java yığın çerçevelerinden referans verilen nesneler).
- Etkin olarak çalışan yöntemlere sahip sınıflar.
- JNI referansları (yerel kod tarafından tutulan genel veya yerel referanslar).
GC köküne giden yol
Bir GC kökünden bir nesneye referans zinciri olduğu sürece bu nesne "erişilebilir" durumdadır ve çöp toplama işlemine tabi tutulamaz. Bu zincire GC köküne giden yol denir. Bellek sızıntısını düzeltmek için bu zinciri belirleyip kırmanız gerekir.

Dominator ağaçları
GC köküne giden yol, bir nesnenin neden canlı olduğunu gösterse de bu referans bozulursa ne kadar bellek geri kazanılacağını göstermez. Bunun için Dominator Trees'i kullanırız.
Herhangi bir GC kökünden B'ye giden her yol A'dan geçmek zorundaysa A nesnesinin B nesnesine hakim olduğu söylenir. A, B'ye hakimse A'nın geri alınması, B'nin de geri alınabileceğini garanti eder. Bunun nedeni, herhangi bir kökten B'ye giden başka yol olmamasıdır.
Aşağıdaki şemada bir nesne grafiği ve buna karşılık gelen baskın öğe ağacı gösterilmektedir. Grafikte D nesnesine hem A hem de B tarafından ulaşıldığını, dolayısıyla A veya B'nin D üzerinde baskın olmadığını, bunun yerine GC Root'un en yakın baskın olduğunu unutmayın.

Java yığın dökümlerini alma
Yığın dökümü, belirli bir anda Java yığınındaki tüm nesnelerin anlık görüntüsüdür.
ADB'yi kullanma
Çalışan bir işlemden yığın dökümü almak için paket adını doğrudan am dumpheap'ya iletebilirsiniz. Bu komutu çalıştırmak için uygulamanızı <profileable android:shell="true"/> veya <debuggable> ile oluşturmanız gerekir.
# 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 .
Perfetto'yu kullanma
Perfetto, Perfetto yapılandırmanızda android.java_hprof veri kaynağını etkinleştirerek sistem genelinde izlemenin bir parçası olarak Java yığın dökümlerini de yakalayabilir. Bu, yığın durumunu diğer sistem etkinlikleriyle ilişkilendirmek için kullanışlıdır.
Perfetto'yu kullanarak MemoryLab uygulaması için yığın dökümü almak üzere aşağıdaki komutu kullanabilirsiniz:
# 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
Perfetto'daki Java yığın dökümleri başlıklı makaleyi inceleyin.
AHAT ile analiz etme
AHAT (Android Heap Analysis Tool), .hprof
dosyalarını web tarayıcısında görüntülemek için önerilen araçtır.
AHAT'ı başlatma
Yolunuza ahat yüklediyseniz şu komutla başlatın:
ahat heap.hprof
Alternatif olarak, bağımsız JAR'ı çalıştırın:
java -jar ahat.jar heap.hprof
Ardından tarayıcınızda http://localhost:7100 adresini açın.
AHAT'ı edinme veya oluşturma hakkında ayrıntılı bilgi için AHAT kaynak deposuna bakın.
Temel analiz iş akışları
Sızıntıları bulma
Ayrılanlar görünümünde Etkinlik sınıfınızı (MainActivity) bulun.

Tüm örnekleri bulmak için sınıfı tıklayın.
İncelemek için MainActivity örneğini tıklayın.

Örnek görünümünde, nesnenin çöp toplama işlemine tabi tutulmasını engelleyen referans zincirini gösteren GC kökünden örnek yol ve bu belirli örnek tarafından ne kadar bellek tutulduğunu gösteren Nesne boyutu'nu bulabilirsiniz.

Bit eşlemlerini analiz etme
AHAT, genellikle büyük bellek tüketicileri olan android.graphics.Bitmap nesnelerinin görüntülenmesi için özel destek sunar. İçeriğinin oluşturulmuş önizlemesini görmek için bir Bitmap örneğini tıklayın.

Etkinlik sızıntıları sayfası
AHAT, Android'deki en yaygın ve etkili bellek sızıntılarından biri olan sızdırılmış Etkinlikleri tanımlamak için özel bir görünüme sahiptir.
- İşlem: MemoryLab'de Etkinlik Sızdır'a dokunun. Bu işlem, kendini kasıtlı olarak sızdıran
LeakedActivityuygulamasını başlatır. - Dump: Yığın dökümü alın.
- Analiz etme: AHAT kenar çubuğunda �Etkinlik Sızıntıları'nı tıklayın.
- Doğrulama: AHAT,
com.android.memorylab.LeakedActivityöğesini sızdırılmış olarak listeler. Bunun nedeni,mDestroyedalanının doğru olması (Etkinlik yaşam döngüsünün sona erdiğini gösterir) ancak öğeye GC kökünden hâlâ erişilebilmesidir.

Yığın dökümlerini karşılaştırma
İki yığın dökümünü karşılaştırmak, bellek sorunlarını belirlemenin en etkili yollarından biridir. "Temiz" bir temel dökümü, bazı işlemler yapıldıktan sonra alınan bir dökümle karşılaştırarak hangi nesnelerin biriktiğini hemen görebilirsiniz.
Egzersiz: Fark karşılaştırmasıyla sızıntıları belirleme
Temel: MemoryLab'i başlatın ve temel yığın dökümü alın:
adb shell am dumpheap com.android.memorylab /data/local/tmp/base.hprof adb pull /data/local/tmp/base.hprof .İşlem: Uygulamada Java Belleği Ayır(10 MB) seçeneğine birkaç kez dokunun.
Son: İkinci bir yığın dökümü alın:
adb shell am dumpheap com.android.memorylab /data/local/tmp/leaked.hprof adb pull /data/local/tmp/leaked.hprof .Karşılaştırma: İkincisini birincil, ilkini ise referans olarak kullanarak AHAT'ı başlatın:
java -jar out/host/linux-x86/framework/ahat.jar leaked.hprof --baseline base.hprofAnalize Genel Bakış: Genel Bakış sayfasında artık Δ (Delta) sütunu bulunuyor.
appyığını için büyük bir pozitif delta görürsünüz. Bu, önemli bir bellek büyümesi olduğunu gösterir.

- Ayrıntılı İnceleme: Menüde rootlu'yu tıklayın. Bu sayfada, GC köklerinden erişilebilen nesneler tutulan boyutlarına göre sıralanmış şekilde gösterilir. En üstte büyük bir pozitif delta ile
MainActivitysimgesini görürsünüz.

Ayırma yığın izlemelerini kaydetme
GC kökünden örnek yol, bir nesnenin neden hâlâ canlı olduğunu açıklasa da nasıl oluşturulduğunu açıklamaz. Ayırma yığını izleri, bir nesneyi ayıran tam kod satırını sağlar.
Kavram ve Değişkenler: Her ayırmanın yığın izleme (stack trace) kaydetmek, hesaplama açısından maliyetlidir ve önemli ölçüde bellek tüketir. Büyük bir üretim uygulamasında bu durum, uygulamayı neredeyse kullanılamaz hale getirebilir. Ancak MemoryLab, tahsislerin kaynağını belirlemek için bu izlemeyi güvenli bir şekilde etkinleştirebileceğimiz kadar küçük bir uygulamadır.
Alıştırma: Bayt dizilerinin kaynağını belirleme
İzlemeyle başlayın: MemoryLab'i durdurmaya zorlayın ve
--track-allocationişaretiyle yeniden başlatın. Daha fazla bağlam elde etmek için varsayılan yığın derinliğini artırın.# 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İşlem: Java Belleği Ayır(10 MB)'a birkaç kez dokunun.
Dump: Yığın dökümü alıp çekin.
Analiz et: Dökümü AHAT'ta açın. Büyük bir
byte[]örneğine gidin. (ör.MainActivity→mJavaAllocations(ArrayList) →elementData(Object[]) → dizi öğesi[0]öğesini inceleyin).Doğrulama: Örnek görünümünde Ayrılan Site bölümüne bakın. Bu durumda,
MainActivity.allocateJavaile sonuçlanan tam yığın izi gösterilir.

Yinelenen dizeleri bulma ve hidrasyon şişkinliği
Bir uygulamada klasik GC-root sızıntıları olmasa bile canlı Java yığını,
, Protobuf Cursor veya Room veritabanı seri durumdan çıkarma sırasında oluşturulan binlerce yinelenen java.lang.String örneği nedeniyle
şişebilir. Tekrarlanan anahtarlar, durum dizeleri, kategori etiketleri veya URL'ler genellikle her ağ yanıtında ya da veritabanı sorgusunda yeniden ayrılır. Büyük feed, mesajlaşma ve içerik uygulamalarında, yinelenen dizeler rutin olarak canlı String belleğin% 30 ila% 60'ını oluşturur.
AHAT'ta yinelenen dizeleri incelemek için:
- Tahsisler sayfasını açın ve
java.lang.Stringile filtreleyin. --baselineile iki yığın dökümünü karşılaştırırken bir feed'i doldurduktan veya yerel bir önbelleği yükledikten sonrajava.lang.Stringörnek sayılarının ve toplam baytların orantısız bir şekilde artıp artmadığını kontrol edin.- Birden fazla bellek içi model nesnesinde tutulan aynı dize değerlerini tespit etmek için
java.lang.Stringörnekler tablosuna (boyuta veya değere göre sıralanmış) göz atın.
- Çözüm: Çalışma zamanı dahili tablosu genel olduğundan ve kilit çekişmesine neden olabileceğinden veya dizeleri gerekenden daha uzun süre saklayabileceğinden, rastgele kullanıcı ya da ağ girişinde
String.intern()işlevini rastgele çağırmaktan kaçının. Bunun yerine, sınırlı kapsamlı bir tekilleştirme önbelleği (ör. ayrıştırıcınızın veya bağdaştırıcınızın içindekiLruCache<String, String>) kullanarak seri durumdan çıkarma sırasında yüksek sıklıklı alan dizelerini tekilleştirin ya da sabit değer kümelerini enum'lar veya tam sayı sabitleri olarak temsil edin.
Java bellek dinamiklerini analiz etme (birleştirilmiş profil)
Bir uygulamanın bellek davranışı hakkında kapsamlı bilgi edinmek için bellek sayaçlarını, iş parçacığı etkinliğini ve çağrı yığınına dayalı ayırma profili oluşturmayı tek bir Perfetto izinde birleştirebilirsiniz. Bu sayede, sistem genelindeki bellek metriklerini (ör. RSS ve yığın boyutu) belirli kod yürütme ve ayırma siteleriyle ilişkilendirebilirsiniz.
Aşağıdakileri sağlayan birleşik bir yapılandırma kullanacağız:
- Bellek sayaçları (
linux.process_stats): RSS ve diğer bellek metriklerini yoklar. - ATrace (
dalvik,memory,schedkategorileri): İş parçacığı durumlarını ve GC etkinliklerini yakalar. - Heapprofd (
android.heapprofd): 5 saniyede bir sürekli dökümlerle hemcom.android.art(Java) hem delibc.malloc(yerel) yığınları hedefler.
Alıştırma: Birleşik bellek analizi
Bu alıştırmada, MemoryLab uygulamasını çalıştırıp izdeki farklı kalıpları gözlemlemek için bir dizi bellek işlemi gerçekleştireceğiz:
- Temel: Boşta kalma durumu.
- Java Churn: Hemen çöp toplama işlemine tabi tutulan geçici tahsisler.
- Kalıcı Java Ayırma: Bellekte kalan Java nesnelerini ayırma.
- Bit eşlem Ayırma: Büyük grafik öğeleri ayırma (yerel yığın/ekran kartı belleğinde bulunur).
- Geri alma: Ayrılan tüm kaynakların serbest bırakılması.
1. Lansman ve hazırlık
Temiz bir durum sağlamak için uygulamayı kapanmaya zorlayın ve yeniden başlatın:
adb shell am force-stop com.android.memorylab adb shell am start -W -n com.android.memorylab/.MainActivity
2. İzlemeyi başlatma ve sırayı tetikleme
40 saniyelik bir izleme başlatıp am
broadcast komutlarını kullanarak bellek etkinliklerini tetikleyeceğiz.
İzlemeyi başlatın:
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 EOFSırayı tetikleyin (izleme çalışırken bu komutları ana makine terminalinizde önerilen zamanlamaya uyarak çalıştırın):
# 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 (KSA Aracı): Profili doğrudan
heap_profilekomut dosyasını kullanarak da başlatabilirsiniz. Bu komut dosyası, sürekli dökümlerle hem Java hem de yerel yığınları hedefler:external/perfetto/tools/heap_profile -n com.android.memorylab \ --heaps com.android.art,libc.malloc \ -c 5000 \ -d 40000 \ -o java_memory_profile
3. Birleştirilmiş izi analiz etme
Toplanan java_memory.perfetto-trace dosyasını Perfetto kullanıcı arayüzünde açın.
Perfetto'daki temel izler
Zaman çizelgesini analiz etmeden önce com.android.memorylab işlemi için gerekli olan şu parçaları bulun:
mem.rss.anon(Anonim RSS): İşlemin Bellek bölümünde bulunur. Bu izleme, işletim sistemi tarafından işleme ayrılan fiziksel belleği (RAM) ölçer. Gerçek bellek kullanımını gösterir.Heap size (KB): Bellek bölümünde de yer alır. Bu, Java yığını için ayrılmış sanal adres alanını temsil eden Dalvik/ART'ye özgü bir sayaçtır. Nesneler ayrıldıkça ve GC çalıştıkça dalgalanan VM'nin dahili yığın sınırını yansıtır.HeapTaskDaemon: İşlem altındaki ileti dizileri listesinde bulunur. Bu, ART çöp toplayıcının işinin büyük bir kısmını yaptığı arka plan iş parçacığıdır. Buradaki etkinlik, etkin GC kartlarını gösterir.- Sürekli ayırma dökümleri (heapprofd): Üst zaman çizelgesi boyunca renkli dilimler olarak gösterilir. Her dilim, zaman içindeki bir süreyi temsil eder. Tek bir dilimi tıkladığınızda veya bir zaman aralığı seçtiğinizde, bu dönemde ne ayrıldığını görmek için com.android.art (Java ayırmaları) veya libc.malloc (yerel ayırmalar) için alev grafiğini (alt bölmede) inceleyebilirsiniz.
Kronolojik aşama analizi
Bu parçaların egzersizin her aşamasında nasıl etkileşime girdiğini görmek için izlemeyi kronolojik olarak inceleyelim.
1. aşama: referans noktası (0-5 saniye)
- Ne oluyor?: Uygulama boşta duruyor ve komut bekliyor.
- Parça Durumu:
mem.rss.anon: Taban çizgisi üzerinde düz çizgi (cihaza bağlı olarak genellikle 60-80 MB civarında).Heap size (KB): İlk Java yığın ayırma işlemiyle eşleşen düz çizgi.HeapTaskDaemon: Boşta (yürütme gösteren dilim yok).- Ayrım Dökümleri: Minimum temel ayırmaları gösterir.

2. aşama: Java ayırma karmaşası (5 sn - 15 sn)
- Neler oluyor?:
AllocationChurnThreadbaşlatılıyor, 1 MB'lık diziler tekrar tekrar ayrılıyor ve atılıyor. - Parça Durumu:
Heap size (KB): Hızlı bir testeredişi deseni gösteriyor. Yığın boyutu, tahsisler biriktikçe artar ve GC çalıştırıldığında keskin bir şekilde düşer.HeapTaskDaemon: Yürütme dilimlerininHeap sizetestere dişindeki düşüşlerle mükemmel şekilde hizalandığı, neredeyse sürekli bir etkinlik gösterir.mem.rss.anon: Java yığın etkinliğini izler.- Java Yığın Ayırma Dökümleri: Bu parçadaki dilimleri seçtiğinizde com.android.art yığın ayırmaları gösterilir.
Ayrılan yer örnekleri, AllocationChurnThread öğesinin birincil yer ayırıcı olduğunu gösteriyor.
Tüm ayrılan yerler, MainActivity.java içindeki lambda'yı işaret eden aynı çağrı yığınını paylaşıyor.

3. aşama: Kalıcı Java ayırma (15-20 saniye)
- Ne oluyor?: 10 MB'lık Java nesnesi ayırıyoruz ve
mJavaAllocationsiçinde bunlara referans tutuyoruz. - Parça Durumu:
Heap size (KB): Testere dişi şeklindeki adımların tabanı yaklaşık 10 MB artıyor.mem.rss.anon: İşletim sistemi bu kalıcı ayırmayı yeni fiziksel sayfalarla desteklemesi gerektiğinden yaklaşık 10 MB artar.- Java Yığın Ayırma Dökümleri: Bu parçadaki dilimleri seçtiğinizde com.android.art yığın ayırmaları gösterilir.
- Allocation Dumps (Flamegraph): Bu pencerede alınan döküm için com.android.art
heap'i incelediğimizde, tutulan boyuta katkıda bulunan
MainActivity.allocateJavaile başlayan yeni bir ayırma yolu gösteriliyor.
Kalıcı ayırma için 10 MB'lık artışla çakışan bir süreyi kapsayan bir ayırma örneği seçin. Ayrılan çağrı yığınlarının iki farklı siteye ayrıldığını görmelisiniz. Bunlardan biri, daha önce gördüğümüz kısa ömürlü aynı ayrılan alan değişiminden, diğeri ise yeni uzun ömürlü ayrılan alandan sorumludur.

4. aşama: bit eşlem ayırma (20-30 saniye)
- Ne oluyor? Ayrıca 20 MB bit eşlem de ayırıyoruz.
- Parça Durumu:
Heap size (KB): Öncekiyle aynı.mem.rss.anon: Bitmap piksel verileri için yerel ayırmalara karşılık gelen yaklaşık 20 MB'lık önemli bir artış gösterir.- Allocation Dumps (Flamegraph): Bu kez libc.malloc (Native) yığını için dilimlere odaklanın.
Yerel bellek ayırma çağrı yığınları, yerel grafik kitaplıklarından kaynaklanan bitmap bellek ayırmayı ortaya çıkarır. Bu, yerel ayırma izleme için iyi bir kullanım alanıdır. Çünkü bu Bitmap ayırmalarını Java yığınında görmezsiniz.

5. aşama: geri kazanım (30-40 yaş)
- Ne oluyor?:
FREE_ALLtetikleniyor. Bu işlemde, tüm kalıcı Java ayırmalarına ve bit eşlemlere yapılan referanslar temizleniyor, ardından açık birSystem.gc()gerçekleştiriliyor. - Parça Durumu:
Heap size (KB): Standart seviyeye geri döner.mem.rss.anon: İşletim sisteminin fiziksel sayfaları geri aldığını gösteren düşüş.HeapTaskDaemon: Çöp toplama işlemi yapılırken son bir etkinlik patlaması gösterir.

Geçmiş OOM'leri izleme (ApplicationExitInfo)
LMK'yi gerçekleştiği sırada yakalamak etkin hata ayıklama için harika bir yöntemdir ancak saha telemetrisi için ApplicationExitInfo API'sini kullanabilirsiniz. Bu, uygulamanızın önceki bir oturumda neden sonlandırıldığını öğrenmesini sağlar.
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
}
}
En iyi uygulamalar
- Önce Temel Değer: Uygulama başlatıldıktan sonra ancak test ettiğiniz işlemi gerçekleştirmeden önce her zaman bir "temel değer" yığın dökümü alın.
- AHAT'ın Etkinlik Sızıntıları Sayfasını Kullanma: AHAT, yok edilen ancak bellekte tutulmaya devam eden Etkinlik örneklerini otomatik olarak tanımlayan özel bir Etkinlik Sızıntıları sayfası içerir. Bu genellikle yaygın sızıntıları bulmanın en hızlı yoludur.
- GC köklerine giden yolu kontrol edin: Sızdırılan tüm nesneler için, hangi referansın nesneyi canlı tuttuğunu (ör. statik alan, uzun süren iş parçacığı veya kayıtlı dinleyici) tam olarak anlamak için AHAT'taki Kökten Giden Yol görünümünü kullanın.
← Araçlar | ↑ Yukarı | Bit eşlemler →