مطالعه موردی: اشکال‌زدایی خرابی ناشی از کمبود حافظه

خرابی‌های «کمبود حافظه» (OutOfMemoryError) زمانی رخ می‌دهد که برنامه شما پشته جاوای اختصاص‌یافته به خود را تمام می‌کند و نمی‌تواند تخصیص حافظه جدیدی را انجام دهد. ردیابی‌های پشته‌ای از OutOfMemoryError گزارش‌های خرابی به‌ندرت علت ریشه‌ای را آشکار می‌کنند زیرا آن‌ها فقط فراخوانی تخصیص را نشان می‌دهند که هنگام تمام شدن حافظه ناموفق بوده است، نه اشیایی که بقیه توده را نگه می‌دارند.

ProfilingManager به شما امکان می‌دهد رونوشت‌های پشته جاوا را از دستگاه‌ها در محل و به‌صورت محلی ضبط کنید تا بتوانید نمودار نگهداری شیء را در رابط کاربری Perfetto بررسی کنید و دقیقاً مشخص کنید چه چیزی حافظه را نگه می‌دارد.

سناریو

فرض کنید ابزار گزارش خرابی شما، خوشه درحال رشدی از OutOfMemoryError خرابی را در تولید هنگام تعامل کاربران با OomSampleActivity پرچم‌گذاری می‌کند.

برای بررسی، راه‌انداز TRIGGER_TYPE_OOM را با ProfilingManager ثبت می‌کنید تا سیستم به‌طور خودکار رونوشت پشته Java را هنگام وقوع OutOfMemoryError ضبط کند.

ضبط رونوشت پشته

در Android 17 (سطح ای‌پی‌آی ۳۷) و بالاتر، برای ضبط رونوشت پشته هنگام وقوع ازکارافتادگی OutOfMemoryError، نمایه‌سازی مبتنی بر راه‌انداز را با ProfilingTrigger.TRIGGER_TYPE_OOM پیکربندی کنید. وقتی برنامه‌تان OutOfMemoryError مدیریت‌نشده‌ای را ایجاد می‌کند، سیستم به‌طور خودکار ضبط PROFILING_TYPE_JAVA_HEAP_DUMP جدیدی را شروع می‌کند.

ازآنجایی‌که فرایند برنامه هنگام وقوع خرابی خاتمه می‌یابد، سیستم ذخیره کردن رونوشت پشته را در پس‌زمینه به‌پایان می‌رساند و نتیجه را در صف قرار می‌دهد (به چرخه حیات راه‌انداز مراجعه کنید). بااستفاده از registerForAllProfilingResults، یک شنونده نتیجه سراسری را در Executor پس‌زمینه پس‌از تکمیل راه‌اندازی برنامه ثبت کنید (تا سرعت راه‌اندازی برنامه را کاهش ندهید) تا برنامه شما دفعه بعد که کاربر آن را راه‌اندازی می‌کند، ProfilingResult صف‌بندی‌شده را دریافت کند. برخلاف addProfilingTriggers (که در بازراه‌اندازی‌های برنامه باقی می‌ماند و فقط باید یک‌بار یا هنگام به‌روزرسانی راه‌اندازها فراخوانی شود)، registerForAllProfilingResults باید در هر راه‌اندازی برنامه فراخوانی شود:

کاتلین

@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))
}

جاوا

@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));
}

ابهام‌زدایی نمایه

پس‌از اینکه OutOfMemoryError ضبط را راه‌اندازی کرد و بازخوانی شما ProfilingResult را در راه‌اندازی بعدی برنامه دریافت کرد، فایل نمایه را در سرورتان بارگذاری کنید یا آن را با adb به ماشین توسعه نرم‌افزارتان بکشید. اگر نمایه را از ساخت نسخهٔ پخش که از کوچک کردن و مبهم‌سازی کد R8 استفاده می‌کند ضبط کرده‌اید، آن را قبل‌از تجزیه‌وتحلیل به‌صورت آفلاین در ایستگاه کاری یا سرور خود ابهام‌زدایی کنید. بدون لغو مبهم‌سازی، R8 نام‌های کلاس، فیلد، و روش جاوا و Kotlin را در رونوشت پشته خام مبهم‌سازی می‌کند (برای مثال، نام‌های کلاس اصلی را به a.b تبدیل می‌کند).

از فرمان trace_processor bundle Perfetto با پرچم --proguard-map استفاده کنید تا نمایه ضبط‌شده را همراه با فایل R8 mapping.txt تولیدشده برای همان ساخت نسخه پخش بسته‌بندی کنید:

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

این فرمان deobfuscated_oom.tar را تولید می‌کند، بایگانی غنی‌شده‌ای که نمایه اصلی را با نگاشت‌های ابهام‌زدایی آن بسته‌بندی می‌کند. وقتی این بایگانی را در میانای کاربر Perfetto باز می‌کنید یا با trace_processor آن را پُرسمان می‌کنید، Perfetto به‌طور خودکار همه نام‌های اصلی کلاس، فیلد، و روش را بازیابی می‌کند. برای اطلاعات بیشتر، به سند نمادگذاری و ابهام‌زدایی Perfetto مراجعه کنید.

تجزیه‌وتحلیل رونوشت پشته

می‌توانید رونوشت پشته ابهام‌زدایی‌شده را به‌صورت دستی در رابط کاربری Perfetto بررسی کنید، همان‌طور که در مراحل زیر نشان داده شده است، یا بررسی کمبود حافظه را با عامل هوش مصنوعی خودکارسازی کنید.

رونوشت پشته ابهام‌زدایی‌شده را در Perfetto UI باز کنید و Heapdump Explorer را در نوار پیمایش سمت راست انتخاب کنید. در برگه Flamegraph با Measure روی Object Size تنظیم شده است، سلسله مراتب نگهداری در شکل ۱ بلافاصله برجسته می‌شود.

نمودار شعله‌ای «کاوشگر Heapdump» در میانای کاربر Perfetto که نشان می‌دهد StatsReporter‏ ۸ نمونه از OomSampleActivity را با مجموع ۲۴۰٫۲۸ مبی‌بایت حفظ می‌کند.
شکل ۱. نمودار شعله‌ای رونوشت پشته که ۸ نمونه حفظ‌شده از OomSampleActivity را نشان می‌دهد.

بررسی نمودار شعله در شکل ۱ چندین یافته کلیدی را نشان می‌دهد:

  • Massive memory retention: Out of a 254.79 MiB total heap (root), com.example.sampleapp.OomSampleActivity retains a Cumulative size of 240.28 MiB (94.31% of the entire app heap), most of which sits in a child byte[] array.
  • چند نمونه فعالیت زنده: با انتخاب قاب OomSampleActivity، Self Count: 8 نشان داده می‌شود. بااینکه فقط یک نمونه از OomSampleActivity باید در هر زمان مشخصی روی صفحه وجود داشته باشد، ۸ نمونه جداگانه به‌طور هم‌زمان در حافظه فعال هستند.
  • مسیر شیء نگهدارنده: خواندن قاب‌های اصلی OomSampleActivity در نمودار شعله‌ای زنجیره ارجاعی را نشان می‌دهد که آن نمونه‌ها را زنده نگه می‌دارد: com.example.sampleapp.StatsReporter حاوی java.util.concurrent.CopyOnWriteArrayList (پشتیبانی‌شده توسط java.lang.Object[]) است که مستقیماً به هر ۸ نمونه OomSampleActivity ارجاع می‌دهد.

شناسایی علت ریشه‌ای در کد

با مسیر حفظی که از StatsReporter به OomSampleActivity اشاره می‌کند، هر دو کلاس را در پایگاه کدتان بررسی کنید.

StatsReporter یک تک‌قلو است که فهرستی از StatsListener فراخوانی‌های برگشتی را حفظ می‌کند و به‌طور دوره‌ای به‌روزرسانی‌ها را از یک رشته نظرسنجی پس‌زمینه پست می‌کند:

کاتلین

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 }
        }
    }
}

جاوا

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();
    }
}

سپس، OomSampleActivity را بازرسی کنید:

کاتلین

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"
    }
}

جاوا

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);
    }
}

این کد تأیید می‌کند که چرا StatsReporter ۸ نمونه از OomSampleActivity را حفظ می‌کند:

  1. هر نمونه OomSampleActivity یک میان‌گیر ۳۰ مبی‌بایتی byte[] اختصاص می‌دهد (heavyData در Kotlin،‏ mHeavyData در Java) و خودش را به‌عنوان شنونده در سینگلتون StatsReporter در onCreate ثبت می‌کند.
  2. OomSampleActivity هرگز unregisterListener را هنگام تخریب سیستم فراخوانی نمی‌کند. چون StatsReporter بیشتر از فعالیت عمر می‌کند و مرجع قوی‌ای برای هر شنونده ثبت‌شده نگه می‌دارد، زباله‌روب نمی‌تواند نمونه‌های تخریب‌شده یا آرایه‌های byte[] ۳۰ مبی‌بایتی آن‌ها را بازیابی کند.
  3. هر بار که سیستم OomSampleActivity را ازبین می‌برد و دوباره ایجاد می‌کند، مثلاً درطول چرخش صفحه‌نمایش در چرخه حیات فعالیت Android، یک نمونه ۳۰ مبی‌بایتی دیگر در StatsReporter نشت می‌کند. پس‌از ۷ چرخش (۸ نمونه × ۳۰ مبی‌بایت ≈ ۲۴۰ مبی‌بایت)، پشته پر می‌شود و برنامه با OutOfMemoryError ازکار می‌افتد.

رفع نشتی

برای رفع نشت حافظه، onDestroy را در فعالیت لغو کنید و StatsReporter.getInstance().unregisterListener(this) را فراخوانی کنید تا وقتی سیستم فعالیت را ازبین می‌برد، تک‌قلو مرجع خود را آزاد کند:

کاتلین

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"
    }
}

جاوا

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);
    }
}

خودکارسازی بررسی کمبود حافظه با عامل هوشواره‌ای

به‌جای بازرسی دستی نمودار شعله‌ای و ردیابی ارجاع‌ها در فایل‌ها، می‌توانید از یک عامل کدنویسی هوش مصنوعی پیکربندی‌شده با مهارت هوش مصنوعی Perfetto برای تجزیه‌وتحلیل رونوشت پشته و مرتبط کردن آن با پایگاه کد خود استفاده کنید:

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.

عامل رونوشت پشته را با trace_processor پُرسمان می‌کند، مسیر غالب را از StatsReporter تا نمونه‌های OomSampleActivity نشت‌شده ردیابی می‌کند، و هر دو کلاس را در پایگاه کد شما بازرسی می‌کند. بدون دخالت دستی، به همان نتیجه می‌رسد که OomSampleActivity شنونده‌ای را در onCreate ثبت می‌کند بدون اینکه ثبت آن را لغو کند، و وصله‌ای تولید می‌کند که unregisterListener(this) را در onDestroy اضافه می‌کند (گزارش زیر از یک ضبط جداگانه تولید شده است که در زمان خرابی ۷ نمونه حفظ‌شده داشت):

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

تأیید رفع مشکل

بررسی کنید که نشت به‌صورت محلی در دستگاه آزمایشی یا در نسخه تولید برطرف شده باشد.

اگر مشکل به‌صورت محلی قابل بازتولید است

چون TRIGGER_TYPE_OOM فقط وقتی رونوشت پشته را ضبط می‌کند که OutOfMemoryError رخ دهد، رفع موفقیت‌آمیز مشکل از فعال شدن راه‌انداز جلوگیری می‌کند. برای بازرسی پشته و تأیید اینکه نشت برطرف شده است، صفحه‌نمایش را بچرخانید و سپس یک رونوشت پشته Java درخواستی از خط فرمان بگیرید:

  1. محدودکننده نرخ را غیرفعال کنید (rate_limiter.disabled true) و نتایج موقت را حفظ کنید (delete_temporary_results.disabled true) تا سیستم ضبط خط فرمان را در /data/misc/perfetto-traces/profiling/ نگه دارد.
  2. OomSampleActivityFixed را در دستگاه آزمایشی‌تان باز کنید، صفحه‌نمایش را ۸ بار یا بیشتر بچرخانید تا مطمئن شوید برنامه دیگر ازکار نمی‌افتد، و آن را در پیش‌زمینه نگه دارید.
  3. بااستفاده از فرمان پوسته cmd profiling_service request-profiling، رونوشت پشته جاوا (profiling_type 1) را به‌صورت درخواستی راه‌اندازی کنید:

    adb shell cmd profiling_service request-profiling 1 com.example.sampleapp
    
  4. نمایه حفظ‌شده را از /data/misc/perfetto-traces/profiling/ بکشید و آن را در واسط کاربری Perfetto باز کنید.

نمودار شعله‌ای Perfetto UI Heapdump Explorer پس‌از رفع مشکل، که یک نمونه از OomSampleActivityFixed را نشان می‌دهد که ۳۰٫۰۱ مبی‌بایت را حفظ می‌کند.
شکل ۲. نمودار شعله‌ای رونوشت پشته پس‌از رفع مشکل که یک نمونه از OomSampleActivityFixed را نشان می‌دهد.

همان‌طور که در شکل ۲ نشان داده شده است، پس‌از چندین چرخش صفحه‌نمایش:

  • com.example.sampleapp.OomSampleActivityFixed تعداد خود ۱ دارد به‌جای اینکه در هر چرخش یک نمونه جمع‌آوری کند.
  • اندازه حفظ‌شده تجمعی آن در ۳۰٫۰۱ مبی‌بایت (از مجموع توده ۴۴٫۲۷ مبی‌بایت) ثابت می‌ماند، که تأیید می‌کند جمع‌آوری‌کننده زباله نمونه‌های فعالیت قبلی را بازیابی می‌کند.

اگر بازتولید مشکل به‌صورت محلی دشوار است

وقتی پیمایش پیچیده چندصفحه‌ای، جلسه‌های طولانی، یا داده‌های خاص کاربر باعث ایجاد OutOfMemoryError می‌شود که نمی‌توانید آن را به‌طور مطمئن در دستگاه آزمایشی بازتولید کنید، پس‌از عرضه، اصلاحیه را در نسخه تولیدی اعتبارسنجی کنید:

  • خوشه خرابی OutOfMemoryError را در ابزار گزارش خرابی‌تان به‌عنوان سنجه پراکسی در نسخه‌های پخش پایش کنید تا مطمئن شوید خرابی‌های مربوط به OomSampleActivity کاهش می‌یابد.
  • هر رونوشت پشته TRIGGER_TYPE_OOM جدیدی را که از نسخه به‌روزشده جمع‌آوری شده است بررسی کنید تا مطمئن شوید StatsReporter دیگر نمونه‌های OomSampleActivity تخریب‌شده را حفظ نمی‌کند.