Yetersiz bellek (OutOfMemoryError) kilitlenmeleri, uygulamanız ayrılan Java yığınını tükettiğinde ve yeni bir bellek ayırma işlemini gerçekleştiremediğinde meydana gelir. OutOfMemoryError Crash Reporting kilitlenme raporlarındaki yığın izlemeler (stack trace), nadiren temel nedeni ortaya çıkarır. Bunun nedeni, yığının geri kalanını tutan nesneleri değil, yalnızca bellek bittiğinde başarısız olan ayırma çağrısını göstermeleridir.
ProfilingManager, sahadaki ve yerel cihazlardan Java yığın dökümlerini yakalamanıza olanak tanır. Böylece Perfetto kullanıcı arayüzünde nesne saklama grafiğini inceleyebilir ve belleği nelerin tuttuğunu tespit edebilirsiniz.
Senaryo
Kilitlenme raporlama aracınızın, kullanıcılar OomSampleActivity ile etkileşim kurduğunda üretimde artan sayıda OutOfMemoryError kilitlenmesi işaretlediğini varsayalım.
Araştırmak için ProfilingManager ile bir TRIGGER_TYPE_OOM tetikleyicisi kaydedersiniz. Böylece sistem, OutOfMemoryError gerçekleştiğinde otomatik olarak bir Java yığın dökümü yakalar.
Yığın dökümünü oluşturma
Android 17 (API düzeyi 37) ve sonraki sürümlerde, OutOfMemoryError kilitlenmesi meydana geldiğinde yığın dökümü yakalamak için ProfilingTrigger.TRIGGER_TYPE_OOM ile tetikleyici tabanlı profil oluşturmayı yapılandırın. Uygulamanız işlenmemiş bir OutOfMemoryError hatası verdiğinde sistem otomatik olarak yeni bir PROFILING_TYPE_JAVA_HEAP_DUMP yakalama işlemi başlatır.
Uygulama işlemi kilitlenme gerçekleştiğinde sonlandırıldığından sistem, yığın dökümünü arka planda kaydetmeyi tamamlar ve sonucu sıraya alır (Tetikleyici yaşam döngüsü bölümüne bakın). Uygulama başlatma işlemi tamamlandıktan sonra (uygulama başlatma işlemini yavaşlatmamak için) registerForAllProfilingResults kullanarak arka plandaki Executor için genel bir sonuç dinleyici kaydedin. Böylece uygulamanız, kullanıcı uygulamayı bir sonraki başlatışında sıraya alınmış ProfilingResult öğesini alır. addProfilingTriggers (uygulama yeniden başlatıldığında da devam eder ve yalnızca bir kez veya tetikleyiciler güncellenirken çağrılması gerekir) yönteminden farklı olarak registerForAllProfilingResults, uygulama her başlatıldığında çağrılmalıdır:
Kotlin
@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN) fun registerOomTrigger(context: Context) { val profilingManager = context.getSystemService(ProfilingManager::class.java) ?: return val callbackExecutor: Executor = Executors.newSingleThreadExecutor() val resultCallback = Consumer<ProfilingResult> { profilingResult -> if ( profilingResult.errorCode == ProfilingResult.ERROR_NONE && profilingResult.triggerType == ProfilingTrigger.TRIGGER_TYPE_OOM ) { val heapDumpPath = profilingResult.resultFilePath // Schedule a background job to compress and upload heapDumpPath } } profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback) val oomTrigger = ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM) .build() profilingManager.addProfilingTriggers(listOf(oomTrigger)) }
Java
@RequiresApi(Build.VERSION_CODES.CINNAMON_BUN) public static void registerOomTrigger(Context context) { ProfilingManager profilingManager = context.getSystemService(ProfilingManager.class); if (profilingManager == null) return; Executor callbackExecutor = Executors.newSingleThreadExecutor(); Consumer<ProfilingResult> resultCallback = profilingResult -> { if (profilingResult.getErrorCode() == ProfilingResult.ERROR_NONE && profilingResult.getTriggerType() == ProfilingTrigger.TRIGGER_TYPE_OOM) { String heapDumpPath = profilingResult.getResultFilePath(); // Schedule a background job to compress and upload heapDumpPath } }; profilingManager.registerForAllProfilingResults(callbackExecutor, resultCallback); ProfilingTrigger oomTrigger = new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_OOM) .build(); profilingManager.addProfilingTriggers(Collections.singletonList(oomTrigger)); }
Profilin kodunu gösterme
Bir OutOfMemoryError, yakalama işlemini tetikledikten ve geri çağırma işleviniz bir sonraki uygulama başlatma işleminde ProfilingResult değerini aldıktan sonra profil dosyasını sunucunuza yükleyin veya adb ile geliştirme makinenize çekin. Profili, R8 kod daraltma ve kod karartma kullanan bir yayınlanan derlemeden aldıysanız analizden önce iş istasyonunuzda veya sunucunuzda çevrimdışı olarak kod gösterme işlemini yapın. R8, kod gösterme işlemi olmadan ham yığın dökümündeki Java ve Kotlin sınıf, alan ve yöntem adlarını kod karartır (ör. orijinal sınıf adlarını a.b olarak değiştirir).
Yakalanan profili, ilgili yayınlanan derleme için oluşturulan R8 mapping.txt dosyasıyla birlikte paketlemek için Perfetto'nun trace_processor bundle komutunu --proguard-map işaretiyle birlikte kullanın:
trace_processor bundle \
--proguard-map /path/to/mapping.txt \
profile_trigger-type-7_2026-05-06-14-12-40-748_uid-10316.perfetto-java-heap-dump \
deobfuscated_oom.tar
Bu komut, orijinal profili kod gösterme eşlemeleriyle birleştiren zenginleştirilmiş bir arşiv olan deobfuscated_oom.tar dosyasını oluşturur. Bu arşivi Perfetto kullanıcı arayüzünde açtığınızda veya trace_processor ile sorguladığınızda Perfetto, tüm orijinal sınıf, alan ve yöntem adlarını otomatik olarak geri yükler. Daha fazla bilgi için Perfetto Simgeselleştirme ve kod gösterme belgelerine bakın.
Yığın dökümünü analiz etme
Aşağıdaki adımlarda gösterildiği gibi, kodları çözülmüş yığın dökümünü Perfetto kullanıcı arayüzünde manuel olarak inceleyebilir veya OOM incelemesini bir yapay zeka temsilcisiyle otomatikleştirebilirsiniz.
Perfetto kullanıcı arayüzünde, kodları çözülmüş yığın dökümünü açın ve soldaki gezinme çubuğunda Yığın Dökümü Gezgini'ni seçin. Ölçüm'ün Nesne Boyutu olarak ayarlandığı Flamegraph sekmesinde, Şekil 1'deki saklama hiyerarşisi hemen göze çarpar.
OomSampleActivity sınıfının 8 adet tutulan örneğini gösteren yığın dökümü flame grafiği.Şekil 1'deki alev grafiği incelendiğinde birkaç önemli bulgu ortaya çıkar:
- Büyük bellek tutma: 254,79 MiB toplam yığından (
root),com.example.sampleapp.OomSampleActivity240,28 MiB (tüm uygulama yığınının %94,31'i) kümülatif boyutunu korur. Bunun çoğu altbyte[]dizisinde bulunur. - Birden fazla canlı etkinlik örneği:
OomSampleActivitykaresi seçildiğinde Öz Sayım: 8 gösterilir. Ekranda aynı anda yalnızca birOomSampleActivityörneği bulunması gerekse de bellekte aynı anda 8 ayrı örnek etkin durumda. - Tutma nesnesi yolu: Flame grafiğinde
OomSampleActivityöğesinin üst çerçevelerini okuduğunuzda bu örnekleri canlı tutan referans zinciri gösterilir:com.example.sampleapp.StatsReporter, doğrudan 8OomSampleActivityörneğinin tümüne referans veren birjava.util.concurrent.CopyOnWriteArrayList(java.lang.Object[]tarafından desteklenir) içerir.
Kodda temel nedeni belirleme
Tutma yolu StatsReporter öğesinden OomSampleActivity öğesine işaret ederken kod tabanınızdaki her iki sınıfı da inceleyin.
StatsReporter, StatsListener geri çağırmalarının listesini tutan ve arka plandaki bir yoklama iş parçacığından düzenli olarak güncelleme yayınlayan bir tekildir:
Kotlin
class StatsReporter private constructor() { fun interface StatsListener { fun onReportStats(stats: String) } private val listeners: MutableList<StatsListener> = CopyOnWriteArrayList() private var isReporting = false init { startReporting() } fun registerListener(listener: StatsListener) { if (!listeners.contains(listener)) { listeners.add(listener) } } fun unregisterListener(listener: StatsListener) { listeners.remove(listener) } private fun startReporting() { if (isReporting) return isReporting = true Thread({ while (true) { try { // Poll every 10 seconds Thread.sleep(10000) } catch (e: InterruptedException) { Thread.currentThread().interrupt() break } for (listener in listeners) { // Send periodic mock stats update from the background thread listener.onReportStats("Update from StatsReporter background polling thread") } } }, "StatsReporterPollingThread").start() } companion object { private var instance: StatsReporter? = null @JvmStatic fun getInstance(): StatsReporter { return instance ?: StatsReporter().also { instance = it } } } }
Java
public class StatsReporter { public interface StatsListener { void onReportStats(String stats); } private static StatsReporter sInstance; private final List<StatsListener> mListeners = new CopyOnWriteArrayList<>(); private boolean mIsReporting = false; private StatsReporter() { startReporting(); } public static StatsReporter getInstance() { if (sInstance == null) { sInstance = new StatsReporter(); } return sInstance; } public void registerListener(StatsListener listener) { if (!mListeners.contains(listener)) { mListeners.add(listener); } } public void unregisterListener(StatsListener listener) { mListeners.remove(listener); } private void startReporting() { if (mIsReporting) return; mIsReporting = true; new Thread(() -> { while (true) { try { // Poll every 10 seconds Thread.sleep(10000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } for (StatsListener listener : mListeners) { // Send periodic mock stats update from the background thread listener.onReportStats("Update from StatsReporter background polling thread"); } } }, "StatsReporterPollingThread").start(); } }
Ardından, OomSampleActivity öğesini inceleyin:
Kotlin
class OomSampleActivity : Activity(), StatsReporter.StatsListener { // Allocate some memory. private val heavyData = ByteArray(30 * 1024 * 1024) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_oom_sample) StatsReporter.getInstance().registerListener(this) } override fun onReportStats(stats: String) { // Read from heavyData so R8 doesn't strip it out as dead code during release builds Log.d(TAG, "Received stats in heavy leaked activity: $stats | Data size: ${heavyData.size}") } companion object { private const val TAG = "OomSampleActivity" } }
Java
public class OomSampleActivity extends Activity implements StatsReporter.StatsListener { private static final String TAG = "OomSampleActivity"; // Allocate some memory. private final byte[] mHeavyData = new byte[30 * 1024 * 1024]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_oom_sample); StatsReporter.getInstance().registerListener(this); } @Override public void onReportStats(String stats) { // Read from mHeavyData so R8 does not strip it out as dead code during release builds Log.d(TAG, "Received stats in heavy leaked activity: " + stats + " | Data size: " + mHeavyData.length); } }
Kod, StatsReporter öğesinin neden 8 adet OomSampleActivity örneği tuttuğunu doğrular:
- Her
OomSampleActivityörneği, 30 MiBbyte[]arabelleği ayırır (Kotlin'deheavyData, Java'damHeavyData) ve kendisinionCreateiçindeki tekilStatsReporterüzerinde dinleyici olarak kaydeder. - Sistem
unregisterListeneröğesini yok ettiğindeOomSampleActivityhiçbir zaman çağrılmaz.StatsReporter, etkinlikten daha uzun süre etkin kaldığı ve kayıtlı her dinleyiciye güçlü bir referans tuttuğu için çöp toplayıcı, imha edilen örnekleri veya bunların 30 MiBbyte[]dizilerini geri kazanamaz. - Sistem,
OomSampleActivityöğesini her imha edip yeniden oluşturduğunda (ör. Android etkinlik yaşam döngüsünde ekran döndürme sırasında)StatsReporteriçinde başka bir 30 MiB örneği sızdırılır. 7 döndürmeden sonra (8 örnek × 30 MiB ≈ 240 MiB) yığın dolar ve uygulamaOutOfMemoryErrorile kilitlenir.
Sızıntıyı giderme
Bellek sızıntısını düzeltmek için etkinlikte onDestroy yöntemini geçersiz kılın ve StatsReporter.getInstance().unregisterListener(this) yöntemini çağırın. Böylece sistem etkinliği yok ettiğinde singleton, referansını serbest bırakır:
Kotlin
class OomSampleActivityFixed : Activity(), StatsReporter.StatsListener { // Allocate some memory. private val heavyData = ByteArray(30 * 1024 * 1024) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_oom_sample) // Register the activity as a listener StatsReporter.getInstance().registerListener(this) } override fun onDestroy() { super.onDestroy() // FIX: Unregister the listener to prevent the memory leak // when the Activity is destroyed (for example, during screen rotation). StatsReporter.getInstance().unregisterListener(this) } override fun onReportStats(stats: String) { // Read from heavyData so R8 doesn't strip it out as dead code during release builds Log.d(TAG, "Received stats in fixed heavy activity: $stats | Data size: ${heavyData.size}") } companion object { private const val TAG = "OomSampleFixed" } }
Java
public class OomSampleActivityFixed extends Activity implements StatsReporter.StatsListener { private static final String TAG = "OomSampleFixed"; // Allocate some memory. private final byte[] mHeavyData = new byte[30 * 1024 * 1024]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_oom_sample); // Register the activity as a listener StatsReporter.getInstance().registerListener(this); } @Override protected void onDestroy() { super.onDestroy(); // FIX: Properly unregister the listener to prevent the memory leak // when the Activity is destroyed (e.g., during screen rotation). StatsReporter.getInstance().unregisterListener(this); } @Override public void onReportStats(String stats) { // Read from mHeavyData so R8 does not strip it out as dead code during release builds Log.d(TAG, "Received stats in fixed heavy activity: " + stats + " | Data size: " + mHeavyData.length); } }
Yapay zeka ajanıyla OOM incelemesini otomatikleştirme
Flame grafiğini manuel olarak incelemek ve dosyalar arasındaki referansları izlemek yerine, Perfetto AI becerisi ile yapılandırılmış bir yapay zeka kodlama temsilcisi kullanarak yığın dökümünü analiz edebilir ve kod tabanınızla ilişkilendirebilirsiniz:
Analyze the Perfetto OOM heap dump at </path/to/profile>, which corresponds to
the codebase located at </path/to/codebase> using the Perfetto AI skill and find
the root cause of the OutOfMemoryError.
Temsilci, yığın dökümünü trace_processor ile sorgular, StatsReporter ile sızdırılan OomSampleActivity örnekleri arasındaki baskın yolu izler ve kod tabanınızdaki her iki sınıfı da inceler. Manuel müdahale olmadan aynı sonuca ulaşır. OomSampleActivity, onCreate içinde bir dinleyiciyi kaydını silmeden kaydeder ve onDestroy içinde unregisterListener(this) ekleyen bir yama oluşturur (Aşağıdaki rapor, kilitlenme sırasında 7 tutulan örneği olan ayrı bir yakalamadan oluşturulmuştur):
# Perfetto OOM Heap Dump Analysis Report
## 1. Orienting Context & First-Pass Triage
| Property | Value |
| :--- | :--- |
| **Process Name** | `com.example.sampleapp` |
| **Sample Timestamp (`graph_sample_ts`)** | `3433221241585` |
| **Leaf Retained Class** | `byte[]` (7 instances) |
| **Leaf Self Size** | `220,201,044` bytes (~210 MiB) |
### Dominator Tree Retention Path
Querying the `android.memory.heap_graph.dominator_tree` standard library module identifies the following largest dominated class path from GC root to leaf allocation:
[ROOT_JAVA_FRAME] com.example.sampleapp.StatsReporter [1]
-> java.util.concurrent.CopyOnWriteArrayList [1]
-> java.lang.Object[] [1]
-> com.example.sampleapp.OomSampleActivity [7]
-> byte[] [7] (220,201,044 bytes)
## 2. Root Cause Analysis
> **Important:** The `OutOfMemoryError` is caused by an **Activity listener leak**: `OomSampleActivity` (`app/src/main/java/com/example/sampleapp/OomSampleActivity.java#L8-L27`) registers itself as a listener on the long-lived `StatsReporter` (`app/src/main/java/com/example/sampleapp/StatsReporter.java#L6-L61`) singleton in `onCreate()`, but never unregisters itself in `onDestroy()`.
### Step-by-Step Retention Mechanism
1. **Per-Activity 30 MiB Allocation:**
Each instance of `OomSampleActivity` (`OomSampleActivity.java#L11-L12`) allocates a 30 MiB byte array (`30 * 1024 * 1024 = 31,457,280` bytes):
```java
// OomSampleActivity.java:12
private final byte[] mHeavyData = new byte[30 * 1024 * 1024];
```
2. **Strong Reference Registration in `onCreate()`:**
When created, `OomSampleActivity.onCreate()` (`OomSampleActivity.java#L15-L20`) registers `this` with `StatsReporter` (`StatsReporter.java#L29-L33`):
```java
// OomSampleActivity.java:19
StatsReporter.getInstance().registerListener(this);
```
3. **GC Root Pinning via Background Thread & Singleton:**
`StatsReporter` (`StatsReporter.java#L11-L60`) is both a static singleton (`sInstance`) and the owner of an active background thread (`StatsReporterPollingThread`, which holds a `[ROOT_JAVA_FRAME]` GC root reference while sleeping/looping in `startReporting()`). It stores listeners in `mListeners` (`CopyOnWriteArrayList<StatsListener>`).
4. **Leak Accumulation Across Activity Recreations:**
Because `OomSampleActivity` does not override `onDestroy()` to call `StatsReporter.unregisterListener(this)`, every destroyed instance of `OomSampleActivity` remains strongly reachable in `mListeners`. After 7 Activity creations (7 * 30 MiB = 220,200,960 bytes plus array headers = `220,201,044` bytes), the Java heap is exhausted.
---
## 3. Actionable Implementation Plan
### 1. Unregister the Listener in `OomSampleActivity.onDestroy()`
Update `OomSampleActivity.java` to unregister the listener when the `Activity` is destroyed (matching the fix in `OomSampleActivityFixed.java`):
```diff
--- a/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
+++ b/app/src/main/java/com/example/sampleapp/OomSampleActivity.java
@@ -19,4 +19,10 @@
StatsReporter.getInstance().registerListener(this);
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ StatsReporter.getInstance().unregisterListener(this);
+ }
+
@Override
```
### 2. Optional Architectural Hardening in `StatsReporter`
* **Lifecycle-Aware Registration:** Have `StatsReporter.registerListener()` accept a `LifecycleOwner` and attach a `DefaultLifecycleObserver` that automatically invokes `unregisterListener()` on `ON_DESTROY`.
* **Weak References:** Store `WeakReference<StatsListener>` entries in `StatsReporter` and evict cleared references during iteration so forgotten `unregisterListener()` calls cannot retain entire `Activity` hierarchies.Düzeltmeyi doğrulama
Sızıntının yerel olarak bir test cihazında veya üretimde çözüldüğünü doğrulayın.
Sorun yerel olarak yeniden oluşturulabiliyorsa
TRIGGER_TYPE_OOM yalnızca OutOfMemoryError gerçekleştiğinde yığın dökümü yakaladığı için başarılı bir düzeltme, tetikleyicinin etkinleşmesini engeller. Yığını incelemek ve sızıntının giderildiğini doğrulamak için ekranı döndürün ve ardından komut satırından seç-izle bir Java yığın dökümü alın:
- Sıklık sınırlayıcıyı devre dışı bırakın (
rate_limiter.disabled true) ve geçici sonuçları saklayın (delete_temporary_results.disabled true). Böylece sistem, komut satırı yakalamayı/data/misc/perfetto-traces/profiling/içinde tutar. - Test cihazınızda
OomSampleActivityFixeduygulamasını açın, ekranı en az 8 kez döndürerek uygulamanın artık kilitlenmediğini doğrulayın ve uygulamayı ön planda tutun. cmd profiling_service request-profilingkabuk komutunu kullanarak isteğe bağlı bir Java yığın dökümü (profiling_type1) tetikleyin:adb shell cmd profiling_service request-profiling 1 com.example.sampleapp/data/misc/perfetto-traces/profiling/konumundan saklanan profili alın ve Perfetto kullanıcı arayüzünde açın.
OomSampleActivityFixed örneğini gösteren yığın dökümü alev grafiği.Şekil 2'de gösterildiği gibi, ekran birden fazla kez döndürüldükten sonra:
com.example.sampleapp.OomSampleActivityFixed, her rotasyonda bir örnek biriktirmek yerine 1 olan bir kendi sayımına sahiptir.- Kümülatif tutulan boyutu 30,01 MiB (toplam 44,27 MiB yığından) olarak sabit kalır. Bu da çöp toplayıcının önceki etkinlik örneklerini geri aldığını doğrular.
Sorunu yerel olarak yeniden oluşturmak zorsa
Karmaşık çok ekranlı gezinme, uzun süreli oturumlar veya belirli kullanıcı verileri, test cihazında güvenilir bir şekilde yeniden oluşturamayacağınız bir OutOfMemoryError hatasına neden olduğunda, düzeltmeyi kullanıma sunduktan sonra üretimde doğrulayın:
OomSampleActivityile ilgili kilitlenmelerin azaldığını doğrulamak için sürümler arasında proxy metriği olarak kilitlenme raporlama aracınızdaOutOfMemoryErrorkilitlenme kümesini izleyin.- Güncellenen sürümden toplanan yeni
TRIGGER_TYPE_OOMyığın dökümlerini inceleyerekStatsReporteröğesinin artık imha edilmişOomSampleActivityörneklerini tutmadığını doğrulayın.