OutOfMemoryError เป็นข้อยกเว้นร้ายแรงที่เกิดขึ้นเมื่อ Java Virtual Machine (JVM) หรือ Android Runtime (ART) ไม่สามารถจัดสรรหน่วยความจำสำหรับออบเจ็กต์ใหม่เนื่องจากพื้นที่ใน Heap ไม่เพียงพอ ตามข้อมูลจาก Square Engineering พบว่า 70% ของ OutOfMemoryError ในแอปพลิเคชันมือถือเกิดจากการรั่วไหลของหน่วยความจำ ไม่ใช่การเกินขีดจำกัดจริง การทำความเข้าใจ สาเหตุของ OOM เป็นกุญแจสำคัญสู่ความเสถียรของแอปพลิเคชัน
ประเด็นสำคัญ
OutOfMemoryError (OOM) เป็นข้อยกเว้นในตระกูล VirtualMachineError ใน Java/Kotlin ที่บ่งชี้ว่าไม่สามารถจัดสรรหน่วยความจำสำหรับออบเจ็กต์ใหม่ได้ ซึ่งแตกต่างจากข้อยกเว้นแบบตรวจสอบได้ OOM เป็น Error และไม่จำเป็นต้องจัดการผ่าน catch — แม้ว่าในทางเทคนิคจะสามารถจับได้ก็ตาม หลังจากเกิด OOM แอปพลิเคชันมักจะอยู่ในสถานะไม่เสถียร และแนะนำให้ยุติการทำงาน
บน Android แต่ละแอปพลิเคชันมีขีดจำกัด Heap ที่กำหนดโดยผู้ผลิตอุปกรณ์ สำหรับสมาร์ทโฟนสมัยใหม่ที่มี RAM 6+ GB ขีดจำกัดคือ 256–512 MB สำหรับอุปกรณ์ระดับประหยัด — 128–192 MB เมื่อปริมาณรวมของออบเจ็กต์ที่มีชีวิตทั้งหมดเกินขีดจำกัดนี้ ART จะโยน OutOfMemoryError
สิ่งสำคัญ คือต้องเข้าใจ: OOM ไม่ได้หมายความว่าอุปกรณ์หมดหน่วยความจำจริงเสมอไป หมายความว่าแอปพลิเคชันใช้ขีดจำกัด Heap ที่ระบบกำหนดจนหมดแล้ว แอปพลิเคชันอื่นอาจมีหน่วยความจำว่าง แต่แอปพลิเคชันของคุณไม่สามารถใช้งานได้เนื่องจากการแยกกระบวนการใน Android
ห้าสถานการณ์ นำไปสู่ OOM ในแอปพลิเคชันมือถือเป็นประจำ แต่ละสถานการณ์เกี่ยวข้องกับประเภทข้อมูลหรือการดำเนินการเฉพาะ
Bitmap เป็นผู้บริโภคหน่วยความจำหลักในแอปพลิเคชัน Android การโหลดภาพ FullHD (1920 × 1080) ในขนาดดั้งเดิมใช้พื้นที่ 8.3 MB ในรูปแบบ ARGB_8888 หากมีภาพดังกล่าว 50 ภาพใน RecyclerView — นั่นคือ 415 MB ซึ่งเกิน Heap ของอุปกรณ์ใดๆ การโหลดภาพโดยไม่มี inSampleSize รับประกัน OOM บนอุปกรณ์ที่อ่อนแอ
ใช้ Glide หรือ Coil สำหรับการปรับขนาดอัตโนมัติ ไลบรารีเหล่านี้โหลดภาพด้วยขนาดที่ตรงกับ View ไม่ใช่ความละเอียดดั้งเดิม สำหรับการใช้ BitmapFactory.Options โดยตรง ให้ใช้ inSampleSize: คำนวณเป็นเลขยกกำลังสองเพื่อให้ขนาดสุดท้ายไม่เกิน 2048 × 2048 พิกเซล นอกจากนี้ ใช้ RGB_565 แทน ARGB_8888 สำหรับภาพที่ไม่มีความโปร่งใส — ซึ่งจะลดการใช้หน่วยความจำลงครึ่งหนึ่ง
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
การรั่วไหล เพียงครั้งเดียวสองสาม KB จะไม่ทำให้เกิด OOM แต่การรั่วไหลหลายสิบครั้งบนแต่ละหน้าจอจะสะสม: การเปลี่ยนหน้าจอแต่ละครั้งเพิ่มการรั่วไหล GC ไม่สามารถปลดปล่อยออบเจ็กต์ และ Heap เต็ม รูปแบบทั่วไป: ผู้ใช้เปิดและปิดหน้าจอโปรไฟล์ 20 ครั้ง → Heap เพิ่มขึ้น 200 MB → แอปพลิเคชันล้มเหลวด้วย OOM
ติดตั้ง LeakCanary ในโปรเจกต์เพื่อตรวจจับการรั่วไหลอัตโนมัติ มันจะแสดงออบเจ็กต์ที่รั่วไหลแต่ละรายการพร้อมร่องรอยสแต็กที่แน่นอน หลังจากแก้ไขการรั่วไหลทั้งหมด การใช้ Heap จะคงที่: หลังจากปิดหน้าจอ หน่วยความจำจะกลับสู่ระดับพื้นฐาน
การโหลด ไฟล์ทั้งหมดลงใน byte[] เป็นเส้นทางตรงสู่ OOM ไฟล์ JSON ขนาด 50 MB ระหว่างการแยกวิเคราะห์จะสร้างสตริงที่มีขนาดเท่ากันบวกกับโมเดล DOM ไฟล์วิดีโอที่โหลดในหน่วยความจำ บัฟเฟอร์เสียง และชุดข้อมูล protobuf ขนาดใหญ่ — ทั้งหมดสามารถเกินขีดจำกัด Heap ในการดำเนินการเดียว
ประมวลผลข้อมูลขนาดใหญ่โดยใช้ สตรีม: InputStream พร้อมบัฟเฟอร์ 4–8 KB, ตัวแยกวิเคราะห์ JSON แบบสตรีมมิ่ง (Jackson หรือ Gson กับ JsonReader), MediaCodec สำหรับวิดีโอ อย่าเรียก File.readBytes() บนไฟล์ที่มีขนาดใหญ่กว่า 10% ของ Heap ที่มีอยู่
การสร้าง ออบเจ็กต์อย่างหนักในลูปโดยไม่มี GC ระหว่างกลางสามารถนำไปสู่ OOM โดยเฉพาะบนอุปกรณ์ที่มี Heap ขนาดเล็ก ตัวอย่าง: การสร้าง 100,000 ออบเจ็กต์ใน for-loop ที่ไม่พอดีใน Heap ก่อนที่ GC จะรวบรวมได้ พบได้บ่อยในเกมและโปรแกรมแก้ไขกราฟิก
ใช้ Object Pool สำหรับออบเจ็กต์ที่ถูกสร้างและทำลายจำนวนมาก สำหรับข้อมูลตัวเลข ให้ใช้ชนิดดั้งเดิม (FloatArray แทน List<Float>) RecyclerView กับ ViewHolder Pool แก้ปัญหานี้สำหรับคอมโพเนนต์ UI
การแตกกระจาย เป็นสถานะที่มีหน่วยความจำว่างเพียงพอโดยรวม แต่ไม่มีบล็อกต่อเนื่องสำหรับออบเจ็กต์ใหม่ ART บีบอัด Heap ระหว่าง GC แต่ไม่สำเร็จเสมอไป อาเรย์ขนาดใหญ่ (Bitmap, byte[]) มีความไวต่อการแตกกระจายมากที่สุด
ART บน Android 8+ ใช้ Generational GC ซึ่งลดการแตกกระจายโดยการแยกออบเจ็กต์อายุน้อยและอายุมาก อย่างไรก็ตาม หลีกเลี่ยงการจัดสรรชิ้นส่วนขนาดต่างๆ ในพูลเดียวกัน — พยายามใช้บัฟเฟอร์ที่จัดสรรไว้ล่วงหน้าขนาดคงที่
ขีดจำกัด Heap ใน Android ไม่ใช่ค่าคงที่ — ขึ้นอยู่กับผู้ผลิต รุ่นอุปกรณ์ และเวอร์ชันของระบบปฏิบัติการ Google กำหนดข้อกำหนดขั้นต่ำผ่านเอกสารคำจำกัดความความเข้ากันได้ (CDD) แต่ผู้ผลิตกำหนดค่าจริง
| หมวดหมู่อุปกรณ์ | Heap ทั่วไป | largeHeap |
|---|---|---|
| ประหยัด (1–2 GB RAM) | 128–192 MB | 256–384 MB |
| ระดับกลาง (3–4 GB RAM) | 256–384 MB | 512 MB |
| เรือธง (6+ GB RAM) | 384–512 MB | 768 MB–1 GB |
| แท็บเล็ต (4+ GB RAM) | 256–512 MB | 768 MB |
| Wear OS | 32–64 MB | ไม่มี |
คุณสามารถขอขีดจำกัดที่เพิ่มขึ้นผ่าน android:largeHeap="true" ในแมนิเฟสต์ ใช้ด้วยความระมัดระวัง: การเพิ่ม Heap ไม่ได้แก้ปัญหาการรั่วไหลและอาจทำให้ประสบการณ์ผู้ใช้แย่ลงหากระบบถูกบังคับให้ปิดแอปพลิเคชันอื่นเพื่อเพิ่มหน่วยความจำให้กับแอปของคุณ สำหรับ Wear OS ขีดจำกัด Heap น้อยที่สุด — เพียง 32–64 MB, largeHeap ไม่พร้อมใช้งานที่นี่ และการประหยัดหน่วยความจำสำคัญเป็นสองเท่า
การวินิจฉัย OOM ต้องวิเคราะห์ Heap Dump และทำความเข้าใจว่าออบเจ็กต์ใดใช้หน่วยความจำ Android Studio มีเครื่องมือที่จำเป็นทั้งหมด
ขั้นตอนที่ 1: จับภาพช่วงเวลาที่เกิด OOM ใน Android Memory Profiler คลิก Record memory allocations และดำเนินการสถานการณ์ที่ทำให้เกิดการล้มเหลว Profiler จะแสดงการเพิ่มขึ้นของการจัดสรรก่อน OOM หาก OOM ไม่สามารถทำซ้ำได้ ให้ลด Heap ผ่าน android:smallHeap ในบิลด์ดีบักหรือใช้ DDMS ด้วยการเรียก GC ด้วยตนเอง
ขั้นตอนที่ 2: ถ่าย Heap Dump ที่โหลดสูงสุด (ก่อน OOM) เปิด Dump ใน Android Studio: แท็บ Classes จะเรียงตาม Retained Size ออบเจ็กต์ที่ใหญ่ที่สุดคือ Bitmap, byte[], String สำหรับแต่ละ Bitmap ให้ตรวจสอบขนาด (กว้าง × สูง × 4 ไบต์) และเส้นทางการโหลดผ่าน Stack Trace
ขั้นตอนที่ 3: วิเคราะห์จำนวนออบเจ็กต์ที่ซ้ำกัน หากคุณเห็น Fragment หรือ Activity ที่เหมือนกัน 200 รายการ — นั่นคือการรั่วไหล หาก Bitmap 500 รายการที่มีขนาดเท่ากัน — นั่นคือปัญหาแคชภาพ MAT (Memory Analyzer Tool) ให้การวิเคราะห์เชิงลึกยิ่งขึ้นด้วย Dominator Tree ที่แสดงว่าออบเจ็กต์ใดถือครอง Heap 80%
// คำสั่ง Heap Dump ผ่าน adb
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
กลยุทธ์ การป้องกัน OOM ที่ครอบคลุมประกอบด้วยการป้องกันห้าระดับ: ตั้งแต่การตัดสินใจทางสถาปัตยกรรมไปจนถึงการตรวจสอบในระบบผลิตจริง
ViewModel + Repository แยกข้อมูลออกจาก UI และป้องกันการเก็บรักษา View เมื่อหมุนหน้าจอ ViewModel มีอายุยืนกว่า Activity ข้อมูลไม่สูญหาย และ View สามารถสร้างใหม่ได้โดยไม่ทำซ้ำข้อมูลในหน่วยความจำ ใช้ StateFlow แทน LiveData สำหรับการจัดการสถานะอย่างชัดเจน
Glide เป็นไลบรารีบังคับสำหรับทำงานกับภาพ มันปรับขนาด จัดเก็บแคช (ดิสก์ + หน่วยความจำ) และรีไซเคิล Bitmap โดยอัตโนมัติ กำหนดค่า diskCacheStrategy และ skipMemoryCache สำหรับรายการขนาดใหญ่ สำหรับภาพเคลื่อนไหว ให้ใช้ Glide กับ GIF/WebP — ใช้หน่วยความจำน้อยกว่าลำดับของ Bitmap
Firebase Performance Monitoring ติดตามการใช้หน่วยความจำแบบเรียลไทม์ ตั้งค่าการแจ้งเตือนเมื่อการใช้ Heap เกิน 80% ของขีดจำกัด — นั่นคือสัญญาณให้ตรวจสอบ Crashlytics รวบรวม OOM เป็นข้อยกเว้นและแสดงสถานะ Heap ล่าสุดที่ทราบก่อนการล้มเหลว สำหรับ Android 11+ ให้ใช้ ApplicationExitInfo เพื่อตรวจจับการสิ้นสุดจาก OOM
อย่าลืม ทดสอบแอปพลิเคชันบนอุปกรณ์ที่มี Heap ขั้นต่ำ (128–192 MB) โปรแกรมจำลองที่มีหน้าจอขนาดเล็กและ Heap ขนาดเล็กจะจำลองอุปกรณ์ระดับประหยัด หากแอปพลิเคชันทำงานบนอุปกรณ์ดังกล่าว จะไม่มีปัญหา OOM บนอุปกรณ์เรือธง ใช้ Firebase Test Lab กับอุปกรณ์จริงจากช่วงราคาต่างๆ
// การตรวจสอบ Heap ที่มีอยู่ก่อนดำเนินการหนัก
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // บัฟเฟอร์ 50%
}
คำถามที่พบบ่อย
ในทางเทคนิคได้ แต่ไม่แนะนำ หลังจาก OOM แอปพลิเคชันอยู่ในสถานะไม่เสถียร: การจัดสรรใหม่อาจล้มเหลว และออบเจ็กต์บางส่วนอาจถูกสร้างขึ้นเพียงบางส่วน การดำเนินการที่สมเหตุสมผลเพียงอย่างเดียวใน catch คือการบันทึกและการเริ่ม Activity ใหม่
ขีดจำกัด Heap แตกต่างกันในแต่ละอุปกรณ์ การดำเนินการที่ต้องใช้ 300 MB จะล้มเหลวบนอุปกรณ์ที่มีขีดจำกัด 192 MB แต่จะสำเร็จบนเรือธงที่มี 512 MB ทดสอบบนอุปกรณ์ที่มีสเปกขั้นต่ำเพื่อตรวจจับสถานการณ์ OOM
largeHeap เพิ่มขีดจำกัดแต่ไม่ทำให้แอปพลิเคชันเร็วขึ้น การหยุดชั่วคราวของ GC นานขึ้นเนื่องจากการรวบรวม Heap ขนาดใหญ่ใช้เวลามากขึ้น ระบบอาจปิดแอปพลิเคชันพื้นหลังเพื่อจัดหาหน่วยความจำ ใช้ largeHeap เฉพาะกับแอปพลิเคชันที่ต้องการหน่วยความจำมากอย่างเป็นกลาง (กล้อง, โปรแกรมแก้ไข)
OOM เป็นข้อยกเว้นภายในแอปพลิเคชันเมื่อ Heap ไม่เพียงพอ การฆ่าโดยระบบ (Low Memory Killer) เป็นการตัดสินใจของเคอร์เนล Linux ที่จะฆ่าโพรเซสเพื่อเพิ่มหน่วยความจำให้แอปพลิเคชันอื่น ในการฆ่าโดยระบบ แอปพลิเคชันจะไม่ได้รับข้อยกเว้น — โพรเซสเพียงแค่สิ้นสุด
สูตร: กว้าง × สูง × bytesPerPixel ARGB_8888 = 4 B/พิกเซล, RGB_565 = 2 B/พิกเซล Bitmap FullHD (1920 × 1080) ใน ARGB_8888 = 8.3 MB Bitmap 4K (3840 × 2160) = 33 MB ปรับขนาดภาพให้เป็นขนาดที่จำเป็นสำหรับการแสดงผลบนหน้าจอเสมอ
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม