ข้อขัดข้องเนื่องจากหน่วยความจำไม่เพียงพอ (OutOfMemoryError) เกิดขึ้นเมื่อแอปใช้ฮีป Java ที่จัดสรรไว้จนหมดและไม่สามารถจัดสรรหน่วยความจำใหม่ได้ สแต็กเทรซจากรายงานข้อขัดข้องของ OutOfMemoryError ไม่ค่อยเปิดเผยสาเหตุของปัญหาเนื่องจากแสดงเฉพาะการเรียกการจัดสรรที่ล้มเหลวเมื่อหน่วยความจำหมด ไม่ใช่ออบเจ็กต์ที่เก็บส่วนที่เหลือของฮีป
ProfilingManager ช่วยให้คุณบันทึกฮีปดัมป์ของ Java จากอุปกรณ์ในภาคสนามและในเครื่องได้ เพื่อให้คุณตรวจสอบกราฟการเก็บรักษาออบเจ็กต์ใน Perfetto UI และระบุสิ่งที่ใช้หน่วยความจำอยู่ได้
สถานการณ์
สมมติว่าเครื่องมือรายงานข้อขัดข้องแจ้งว่ามีคลัสเตอร์ข้อขัดข้อง OutOfMemoryError ที่เพิ่มขึ้นในเวอร์ชันที่ใช้งานจริงเมื่อผู้ใช้โต้ตอบกับ OomSampleActivity
หากต้องการตรวจสอบ คุณต้องลงทะเบียนทริกเกอร์ TRIGGER_TYPE_OOM กับ ProfilingManager เพื่อให้ระบบบันทึก Java ฮีปดัมป์ โดยอัตโนมัติเมื่อเกิด OutOfMemoryError
บันทึกฮีปดัมป์
ใน Android 17 (ระดับ API 37) ขึ้นไป หากต้องการบันทึกฮีปดัมป์เมื่อเกิดข้อขัดข้อง OutOfMemoryError ให้กำหนดค่าการสร้างโปรไฟล์ตามทริกเกอร์ด้วย ProfilingTrigger.TRIGGER_TYPE_OOM เมื่อแอปแสดง OutOfMemoryError ที่ไม่ได้รับการจัดการ ระบบจะเริ่มบันทึก PROFILING_TYPE_JAVA_HEAP_DUMP ใหม่โดยอัตโนมัติ
เนื่องจากกระบวนการของแอปจะสิ้นสุดลงเมื่อเกิดข้อขัดข้อง ระบบจึงบันทึกฮีปดัมพ์ในเบื้องหลังให้เสร็จสิ้นและจัดคิวผลลัพธ์ (ดูวงจรทริกเกอร์) ลงทะเบียน Listener ผลลัพธ์ส่วนกลางในเบื้องหลัง Executor
หลังจากที่แอปเริ่มต้นเสร็จสมบูรณ์ (เพื่อไม่ให้แอปเริ่มทำงานช้าลง) โดยใช้
registerForAllProfilingResults เพื่อให้แอปได้รับ ProfilingResult ที่อยู่ในคิว
ในครั้งถัดไปที่ผู้ใช้เปิดแอป registerForAllProfilingResults ต้องเรียกใช้ทุกครั้งที่แอปเริ่มต้น ซึ่งต่างจาก addProfilingTriggers (ซึ่งจะยังคงอยู่แม้จะรีสตาร์ทแอป และต้องเรียกใช้เพียงครั้งเดียวหรือเมื่ออัปเดตทริกเกอร์) ดังนี้
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)); }
ถอดรหัสโปรไฟล์
หลังจาก OutOfMemoryError ทริกเกอร์การบันทึกและ Callback ได้รับ ProfilingResult ในการเปิดแอปครั้งถัดไป ให้อัปโหลดไฟล์โปรไฟล์ไปยังเซิร์ฟเวอร์หรือดึงไฟล์ไปยังคอมพิวเตอร์สำหรับการพัฒนาซอฟต์แวร์ด้วย adb หากคุณบันทึกโปรไฟล์จากบิลด์ที่เผยแพร่ซึ่งใช้การลดขนาดโค้ดและการปรับให้ยากต่อการอ่าน (Obfuscation) ของ R8 ให้ถอดรหัสซอร์สโค้ดที่สร้างความสับสนแบบออฟไลน์ในเวิร์กสเตชันหรือเซิร์ฟเวอร์ก่อนการวิเคราะห์ หากไม่มีการถอดรหัสซอร์สโค้ดที่สร้างความสับสน R8 จะปรับให้ยากต่อการอ่าน (Obfuscate) ชื่อคลาส, ฟิลด์ และเมธอดของ Java และ 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 UI หรือค้นหาด้วย trace_processor Perfetto จะกู้คืนชื่อคลาส ฟิลด์ และเมธอดเดิมทั้งหมดโดยอัตโนมัติ ดูข้อมูลเพิ่มเติมได้ที่เอกสารประกอบการแทนที่ด้วยสัญลักษณ์และการถอดรหัสซอร์สโค้ดที่สร้างความสับสนของ Perfetto
วิเคราะห์ฮีปดัมป์
คุณสามารถตรวจสอบฮีปดัมป์ที่ถอดรหัสแล้วด้วยตนเองใน Perfetto UI ตามขั้นตอนต่อไปนี้ หรือตรวจสอบ OOM โดยอัตโนมัติด้วย AI Agent
เปิดดัมพ์ฮีปที่ถอดรหัสแล้วใน Perfetto UI แล้วเลือก Heapdump Explorer ในแถบนำทางด้านซ้าย ในแท็บกราฟเปลวไฟที่มีการวัดตั้งค่าเป็นขนาดออบเจ็กต์ ลำดับชั้นการเก็บรักษาในรูปที่ 1 จะโดดเด่นขึ้นมาทันที
OomSampleActivityการตรวจสอบกราฟ Flame ในรูปที่ 1 เผยให้เห็นผลลัพธ์ที่สำคัญหลายประการ ดังนี้
- การเก็บรักษาหน่วยความจำจำนวนมาก: จากฮีปรวม 254.79 MiB (
root)com.example.sampleapp.OomSampleActivityจะเก็บรักษาขนาดสะสมไว้ที่ 240.28 MiB (94.31% ของฮีปแอปทั้งหมด) ซึ่งส่วนใหญ่อยู่ในอาร์เรย์byte[]ย่อย - อินสแตนซ์กิจกรรมสดหลายรายการ: การเลือกเฟรม
OomSampleActivityจะแสดง Self Count: 8 แม้ว่าควรมีOomSampleActivityเพียงอินสแตนซ์เดียวบนหน้าจอ ณ เวลาหนึ่งๆ แต่อินสแตนซ์ที่แยกกัน 8 รายการจะทำงานในหน่วยความจำพร้อมกัน - เส้นทางออบเจ็กต์ที่เก็บรักษา: การอ่านเฟรมหลักของ
OomSampleActivityในกราฟ Flame แสดงเชนการอ้างอิงที่เก็บอินสแตนซ์เหล่านั้นไว้ โดยcom.example.sampleapp.StatsReporterเก็บjava.util.concurrent.CopyOnWriteArrayList(ซึ่งได้รับการสนับสนุนโดยjava.lang.Object[]) ซึ่งอ้างอิงอินสแตนซ์OomSampleActivityทั้ง 8 รายการโดยตรง
ระบุสาเหตุของปัญหาในโค้ด
เมื่อเส้นทางการเก็บรักษาชี้จาก StatsReporter ไปยัง OomSampleActivity ให้ตรวจสอบทั้ง 2 คลาสในฐานของโค้ด
StatsReporter เป็นซิงเกิลตันที่เก็บรักษารายการของ Callback StatsListener และโพสต์การอัปเดตเป็นระยะๆ จากเทรดการโพลในเบื้องหลัง
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(); } }
ถัดไป ให้ตรวจสอบ OomSampleActivity ดังนี้
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); } }
โค้ดยืนยันสาเหตุที่ StatsReporter เก็บ OomSampleActivity ไว้ 8 อินสแตนซ์ ดังนี้
- อินสแตนซ์
OomSampleActivityแต่ละรายการจะจัดสรรบัฟเฟอร์byte[]ขนาด 30 MiB (heavyDataใน Kotlin,mHeavyDataใน Java) และลงทะเบียนตัวเองเป็น Listener ใน SingletonStatsReporterในonCreate OomSampleActivityจะไม่เรียกใช้unregisterListenerเมื่อระบบทำลายออบเจ็กต์ เนื่องจากStatsReporterมีอายุการใช้งานนานกว่ากิจกรรมและเก็บการอ้างอิงที่เข้มงวดไปยัง Listener ที่ลงทะเบียนไว้ทุกรายการ ตัวรวบรวมขยะจึงไม่สามารถเรียกคืนอินสแตนซ์ที่ถูกทำลายหรืออาร์เรย์byte[]ขนาด 30 MiB ได้- ทุกครั้งที่ระบบทำลายและสร้าง
OomSampleActivityขึ้นมาใหม่ เช่น ระหว่างการหมุนหน้าจอในวงจรกิจกรรมของ Android อินสแตนซ์ขนาด 30 MiB อีกรายการจะรั่วไหลในStatsReporterหลังจากหมุน 7 ครั้ง (8 อินสแตนซ์ × 30 MiB ≈ 240 MiB) ฮีปจะเต็มและแอปขัดข้องพร้อมข้อผิดพลาดOutOfMemoryError
แก้ไขการรั่วไหล
หากต้องการแก้ไขหน่วยความจำรั่วไหล ให้ลบล้าง onDestroy ในกิจกรรมและเรียกใช้ StatsReporter.getInstance().unregisterListener(this) เพื่อให้ Singleton ปล่อยการอ้างอิงเมื่อระบบทำลายกิจกรรม
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); } }
ทำให้การตรวจสอบ OOM เป็นอัตโนมัติด้วย AI Agent
แทนที่จะตรวจสอบกราฟ Flame และติดตามการอ้างอิงในไฟล์ต่างๆ ด้วยตนเอง คุณสามารถใช้เอเจนต์การเขียนโค้ด AI ที่กำหนดค่าด้วยทักษะ Perfetto AI เพื่อวิเคราะห์ฮีปดัมพ์และเชื่อมโยงกับฐานของโค้ดได้ ดังนี้
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.
Agent จะค้นหาฮีปดัมพ์ด้วย trace_processor ติดตามเส้นทาง Dominator จาก StatsReporter ไปยังอินสแตนซ์ OomSampleActivity ที่รั่วไหล และตรวจสอบทั้ง 2 คลาสในฐานของโค้ด โดยไม่ต้องมีการแทรกแซงด้วยตนเอง ระบบจะสรุปผลแบบเดียวกันว่า OomSampleActivity ลงทะเบียน Listener ใน onCreate โดยไม่ยกเลิกการลงทะเบียน และสร้างแพตช์ที่เพิ่ม unregisterListener(this) ใน onDestroy (รายงานต่อไปนี้สร้างขึ้นจากการบันทึกแยกต่างหากซึ่งมีอินสแตนซ์ที่เก็บไว้ 7 รายการ ณ เวลาที่เกิดข้อขัดข้อง)
# 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 แบบออนดีมานด์จากบรรทัดคำสั่ง โดยทำดังนี้
- ปิดใช้การจำกัดอัตราคำขอ (
rate_limiter.disabled true) และเก็บผลลัพธ์ชั่วคราว (delete_temporary_results.disabled true) เพื่อให้ระบบเก็บการบันทึกบรรทัดคำสั่งไว้ใน/data/misc/perfetto-traces/profiling/ - เปิด
OomSampleActivityFixedในอุปกรณ์ทดสอบ หมุนหน้าจออย่างน้อย 8 ครั้งเพื่อยืนยันว่าแอปไม่ขัดข้องอีกต่อไป และเปิดแอปไว้ในเบื้องหน้า เรียกใช้ฮีปดัมป์ของ Java แบบออนดีมานด์ (
profiling_type1) โดยใช้คำสั่ง Shellcmd profiling_service request-profilingดังนี้adb shell cmd profiling_service request-profiling 1 com.example.sampleappดึงโปรไฟล์ที่เก็บไว้จาก
/data/misc/perfetto-traces/profiling/และเปิดใน Perfetto UI
OomSampleActivityFixedดังที่แสดงในรูปที่ 2 หลังจากหมุนหน้าจอหลายครั้ง
com.example.sampleapp.OomSampleActivityFixedมีจำนวนตัวเองเป็น 1 แทนที่จะสะสมอินสแตนซ์ในการหมุนแต่ละครั้ง- ขนาดที่คงไว้สะสมยังคงคงที่ที่ 30.01 MiB (จากฮีปทั้งหมด 44.27 MiB) ซึ่งยืนยันว่าตัวเก็บขยะจะเรียกคืนอินสแตนซ์กิจกรรมก่อนหน้า
หากปัญหาเกิดขึ้นซ้ำได้ยากในเครื่อง
เมื่อการนําทางหลายหน้าจอที่ซับซ้อน เซสชันที่ใช้เวลานาน หรือข้อมูลผู้ใช้ที่เฉพาะเจาะจงทำให้เกิด OutOfMemoryError ที่คุณไม่สามารถจำลองได้อย่างน่าเชื่อถือในอุปกรณ์ทดสอบ ให้ตรวจสอบการแก้ไขในเวอร์ชันที่ใช้งานจริงหลังจากการเปิดตัว โดยทำดังนี้
- ตรวจสอบคลัสเตอร์ข้อขัดข้อง
OutOfMemoryErrorในเครื่องมือรายงานข้อขัดข้องเป็นเมตริกพร็อกซีในรุ่นต่างๆ เพื่อยืนยันว่าข้อขัดข้องที่เกี่ยวข้องกับOomSampleActivityลดลง - ตรวจสอบฮีปดัมพ์
TRIGGER_TYPE_OOMใหม่ที่รวบรวมจากรุ่นที่อัปเดตเพื่อยืนยันว่าStatsReporterไม่เก็บอินสแตนซ์OomSampleActivityที่ถูกทำลายไว้อีกต่อไป