หน่วยความจำรั่ว: คืออะไร สถานการณ์ทั่วไป และการวินิจฉัย

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-07-29 เวลาอ่าน: 10 นาที

หน่วยความจำรั่ว (memory leak) — สถานการณ์ที่แอปพลิเคชันไม่ปล่อยหน่วยความจำที่ถูกครอบครองโดยวัตถุที่ไม่จำเป็นอีกต่อไป ในการพัฒนามือถือ สิ่งนี้สำคัญอย่างยิ่ง: heap ที่จำกัดและการไม่มี swap นำไปสู่ OutOfMemoryError และแอปพลิเคชันขัดข้อง ตามข้อมูลของ Purdue University (2022) 35% ของแอป Android ใน Google Play มีหน่วยความจำรั่วอย่างน้อยหนึ่งรายการ มาดูสถานการณ์ทั่วไป เครื่องมือวินิจฉัย และวิธีการแก้ไขกัน

ประเด็นสำคัญ

  • GC Root — จุดเข้าที่ตัวเก็บขยะใช้กำหนดวัตถุที่มีชีวิต
  • Context รั่ว — การส่ง Activity Context ไปยังซิงเกิลตันทำให้ลำดับชั้น View ทั้งหมดถูกยึดไว้
  • Handler กับ postDelayed — หาก Activity ถูกทำลาย Handler จะป้องกันไม่ให้ถูก GC
  • Heap dump — วิธีการหลักในการวิเคราะห์การรั่วไหลผ่าน MAT หรือ Android Profiler
  • SoftReference — ทางเลือกแทน WeakReference สำหรับแคชที่ล้างอัตโนมัติเมื่อหน่วยความจำไม่เพียงพอ

หน่วยความจำรั่วในแอปพลิเคชันมือถือคืออะไร?

หน่วยความจำรั่ว — สถานการณ์ที่หน่วยความจำที่ถูกจัดสรรไม่ถูกส่งคืนให้ระบบหลังจากที่วัตถุไม่จำเป็นต่อโปรแกรมอีกต่อไป ตัวเก็บขยะถือว่าวัตถุดังกล่าวมีชีวิตอยู่เนื่องจากมีห่วงโซ่การอ้างอิงที่ทำงานอยู่จาก GC Root ชี้ไปที่มัน

ใน Java/Kotlin ตัวเก็บขยะทำงานโดยอัตโนมัติ แต่ไม่สามารถระบุได้ว่าวัตถุไม่จำเป็นตามตรรกะหากมีการอ้างอิงทางเทคนิคไปยังวัตถุนั้น นักพัฒนาต้องตัดการเชื่อมต่อที่ไม่จำเป็นอย่างชัดเจน ใน Swift/Objective-C ARC นับการอ้างอิงโดยอัตโนมัติ แต่ retain cycles ปิดกั้นไม่ให้ตัวนับถึงศูนย์

อันตรายหลักของการรั่วไหลคือ ผลสะสม การรั่วไหลแต่ละครั้งใช้หน่วยความจำจำนวนเล็กน้อย แต่เมื่อมีการเปลี่ยนหน้าจอซ้ำ ๆ (หมุนหน้าจอ เปิด/ปิด Activity) การรั่วไหลจะสะสมจนกว่าขีดจำกัด heap จะหมด

การรั่วไหลแตกต่างจากการพองตัวอย่างไร?

การรั่วไหล — วัตถุไม่สามารถเข้าถึงได้โดยโค้ดแต่ไม่ได้ถูกลบโดย GC การพองตัว — วัตถุจำเป็นตามตรรกะแต่ถูกเก็บไว้ในปริมาณที่มากเกินไป ตัวอย่างการพองตัว: แคชรูปภาพ 100 MB โดยมีชุดทำงาน 30 MB ปัญหาทั้งสองนำไปสู่ OOM แต่สาเหตุและวิธีการรักษาต่างกัน

ตัวเก็บขยะทำงานอย่างไรและทำไมจึงเกิดการรั่วไหล?

ART (Android Runtime) ใช้การเก็บขยะแบบรุ่นต่อรุ่นพร้อมการบีบอัดพร้อมกัน หน่วยความจำแบ่งออกเป็นรุ่นอายุน้อย (Young) รุ่นแก่ (Old) และวัตถุขนาดใหญ่ (Large) วัตถุที่รอดชีวิตจากหลายรอบ GC จะถูกย้ายไปยังรุ่น Old ซึ่งการเก็บเกิดขึ้นน้อยกว่า — ซึ่งทำให้รอบปกติเร็วขึ้น

GC จะเริ่มเมื่อ heap ถึงเกณฑ์การครอบครองที่กำหนด (โดยปกติ 75-85%) ระหว่าง GC เธรดทั้งหมดของแอปพลิเคชันจะถูกหยุดชั่วคราว (STW — Stop The World) ยิ่งมีวัตถุที่มีชีวิตมากเท่าไร การหยุดชั่วคราวก็ยิ่งนานขึ้นเท่านั้น การรั่วไหลเพิ่มจำนวนวัตถุที่มีชีวิต ทำให้การหยุดชั่วคราวของ GC ยาวนานขึ้น

ตัวเก็บขยะระบุวัตถุที่มีชีวิตโดยการเดินกราฟจาก GC Roots: ฟิลด์สแตติก ตัวแปรสแต็กของเธรดที่ทำงานอยู่ การอ้างอิง JNI วัตถุใด ๆ ที่สามารถเข้าถึงได้ผ่านการอ้างอิงจากรากเหล่านี้ถือว่ามีชีวิตอยู่ — แม้ว่านักพัฒนาจะรู้ว่ามันไม่จำเป็นอีกต่อไป

kotlin
// ตัวอย่าง: คอลเลกชันสแตติกเป็น GC Root — การรั่วไหลถาวร
object GlobalHolder {
    val listeners = mutableListOf<WeakReference<Any>>()
}

class LeakingFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        GlobalHolder.listeners.add(WeakReference(this))
        // WeakReference ไม่ป้องกัน GC — พฤติกรรมที่ถูกต้อง
    }
}

WeakReference แก้ปัญหา: GC ไม่สนใจการอ้างอิงแบบอ่อนเมื่อระบุวัตถุที่มีชีวิต หากเหลือเพียงการอ้างอิงแบบอ่อนไปยังวัตถุ มันจะถูกเก็บในรอบ GC ที่ใกล้ที่สุด

สถานการณ์การรั่วไหลทั่วไปใน Android และ iOS

Activity Context — สถานการณ์การรั่วไหลที่แพร่หลายที่สุดใน Android หากซิงเกิลตัน ฟิลด์สแตติก หรือบริการที่มีอายุยาวนานเก็บการอ้างอิงไปยัง Activity Context ทั้ง Activity ที่มี Views ทั้งหมดจะไม่สามารถถูกเก็บโดย GC ได้ วิธีแก้ไข: ใช้ Application Context สำหรับวัตถุที่มีอายุยาวนาน

Handler และข้อความที่ส่ง — Handler.postDelayed(runnable, delay) วางข้อความในคิว Main Looper หาก Activity ถูกทำลายก่อนที่การหน่วงเวลาจะหมด ข้อความยังคงอยู่ในคิวและรักษาการอ้างอิงผ่าน Runnable → คลาสนิรนาม → คลาสภายนอก (Activity)

kotlin
class SafeActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val callback = Runnable { /* update UI */ }

    override fun onResume() {
        super.onResume()
        mainHandler.postDelayed(callback, 5000)
    }

    override fun onPause() {
        mainHandler.removeCallbacks(callback) // จำเป็น: ล้างคิว
        super.onPause()
    }
}

คลาสภายใน — คลาสภายในที่ไม่ใช่สแตติกมีการอ้างอิงโดยนัยไปยังอินสแตนซ์ของคลาสภายนอก หากคลาสภายนอกเป็น Activity และคลาสภายในถูกส่งออกไปภายนอก (เช่น ไปยัง RecyclerView.Adapter) Activity จะไม่สามารถถูกเก็บได้

  • TimerTask และ ScheduledExecutorService — งานที่ถูกกำหนดเวลาก่อนการทำลาย Activity
  • BroadcastReceiver — ไม่ได้ยกเลิกการลงทะเบียนใน onPause/onDestroy จะยังคงถือ Context ไว้
  • ViewModel ที่มีการอ้างอิงไปยัง View — ViewModel มีอายุยืนกว่า Activity การอ้างอิงไปยัง View นำไปสู่การรั่วไหล
  • Retrofit Call — หาก Call ไม่ถูกยกเลิก การตอบสนองจะมาถึง Fragment ที่ถูกทำลาย

เครื่องมือวินิจฉัยหน่วยความจำรั่ว

Android Studio Memory Profiler — เครื่องมือในตัวสำหรับตรวจสอบ heap แบบเรียลไทม์ แสดงกราฟหน่วยความจำที่ใช้ จำนวนการจัดสรร และวัตถุตามประเภท สามารถบันทึก heap dump และส่งออกในรูปแบบ HPROF สำหรับการวิเคราะห์ใน MAT

Eclipse MAT (Memory Analyzer Tool) — โปรแกรมวิเคราะห์ heap dump บนเดสก์ท็อป สร้างรายงาน Leak Suspects โดยอัตโนมัติ ซึ่งเน้นวัตถุที่มี retained size มากที่สุดและแนะนำ GC root chain ที่เป็นไปได้สำหรับวัตถุที่น่าสงสัยแต่ละรายการ

Xcode Memory Graph Debugger — สำหรับ iOS หยุดแอปพลิเคชันชั่วคราวและแสดงกราฟวัตถุ Retain cycles จะถูกเน้นด้วยสีแดง คุณสามารถคลิกที่วัตถุใดก็ได้เพื่อดู retain count และการอ้างอิง

เครื่องมือความสามารถความซับซ้อน
Memory Profilerกราฟเรียลไทม์, heap dump, การติดตามการจัดสรรวัตถุต่ำ
Eclipse MATDominator tree, Leak Suspects, คำสั่ง OQLปานกลาง
LeakCanaryการตรวจจับอัตโนมัติ, ร่องรอยการรั่วไหลในการแจ้งเตือนน้อยที่สุด
Xcode Memory Graphกราฟภาพ retain cycles, รายการวัตถุที่มีชีวิตต่ำ

ตาม Uber Engineering Blog การรวมการสร้างโปรไฟล์หน่วยความจำอัตโนมัติ (LeakCanary + การวิเคราะห์ heap dump) ในไปป์ไลน์ CI/CD ช่วยลดเหตุการณ์ที่เกี่ยวข้องกับหน่วยความจำในการผลิตลง 60% ภายใน 3 เดือน

วิธีการกำจัดการรั่วไหล

เปลี่ยน Context — หากวัตถุมีอายุยืนกว่า Activity ให้ใช้ applicationContext วัตถุที่มีอายุยาวนานทั้งหมด (ซิงเกิลตัน, รีโพซิทอรี, ตัวช่วยฐานข้อมูล) ควรได้รับ Application Context ไม่ใช่ Activity Context ข้อยกเว้น: ส่วนประกอบ UI ที่ต้องการเข้าถึงธีมหรือทรัพยากรเฉพาะของ Activity

ส่วนประกอบที่รับรู้วงจรชีวิต — การใช้ LifecycleObserver, DefaultLifecycleObserver หรือส่วนขยายแบบรีแอคทีฟจะยกเลิกการสมัครรับข้อมูลโดยอัตโนมัติที่ onDestroy Android Jetpack มี lifecycleScope และ viewModelScope ซึ่งถูกทำความสะอาดโดยเหตุการณ์วงจรชีวิตที่เกี่ยวข้อง

คลาสภายในแบบสแตติก — หากคลาสภายในไม่จำเป็นต้องเข้าถึงฟิลด์ของคลาสภายนอก ให้ทำให้เป็น static คลาสภายในแบบสแตติกไม่มีการอ้างอิงโดยนัยไปยังคลาสภายนอก หากต้องการเข้าถึง ให้ใช้ WeakReference สำหรับการอ้างอิงที่ชัดเจน

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ คลาสภายในที่ไม่ใช่สแตติก — การอ้างอิงโดยนัยไปยัง MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ คลาสภายในแบบสแตติก — ไม่มีการอ้างอิงโดยนัย
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

ใน iOS ใช้รายการดักจับ: [weak self] ใน closures ที่อาจมีอายุยืนกว่าผู้สร้าง สำหรับตัวแทน ใช้การอ้างอิงแบบอ่อน (weak var delegate) สำหรับ closures ที่รับประกันว่าจะถูกเรียกเฉพาะในช่วงอายุของ self สามารถใช้ [unowned self] ได้ แต่ต้องระมัดระวัง — การเข้าถึงวัตถุที่ถูกยกเลิกการจัดสรรจะทำให้เกิดการขัดข้อง

คำถามที่พบบ่อย

จะหาการรั่วไหลโดยไม่มีเครื่องมือพิเศษได้อย่างไร?

ใน Android ทำการเปลี่ยนหน้าจอหลายครั้ง (Activity A → B → A → B) และตรวจสอบ adb shell dumpsys meminfo package_name หาก Total PSS เพิ่มขึ้นอย่างต่อเนื่องและไม่กลับไปยังค่าเดิม — มีการรั่วไหล ใน iOS เช่นเดียวกัน: ใช้ Debug Memory Graph ใน Xcode เพื่อตรวจสอบด้วยภาพ

Kotlin coroutine สามารถทำให้เกิดการรั่วไหลได้หรือไม่?

ได้ หาก CoroutineScope ไม่ถูกยกเลิกเมื่อส่วนประกอบถูกทำลาย Coroutine ที่เริ่มต้นใน GlobalScope จะยังคงทำงานต่อไปแม้หลังจาก finish() ของ Activity วิธีแก้ไข: ใช้ viewModelScope (ยกเลิกใน onCleared) หรือ lifecycleScope (ยกเลิกใน onDestroy) สำหรับขอบเขตที่กำหนดเอง ให้สร้างขอบเขตที่รับรู้วงจรชีวิตผ่าน LifecycleOwner

Bitmap ส่งผลต่อการรั่วไหลอย่างไร?

Bitmap เก็บข้อมูลพิกเซลใน native heap ไม่ใช่ Java heap ซึ่งหมายความว่า Java GC ไม่เห็นขนาดจริงของ Bitmap หากไม่เรียก recycle() บน Bitmap หรือไม่ทำให้การอ้างอิงเป็น null หน่วยความจำ native จะไม่ถูกปล่อย ใช้ BitmapFactory กับ inSampleSize เพื่อโหลดสำเนาที่ลดขนาดลง และ Glide/Coil สำหรับการจัดการแคชอัตโนมัติ

การรั่วไหลผ่านฟิลด์สแตติกคืออะไร?

ฟิลด์สแตติก — คือ GC Root มันมีชีวิตอยู่ตราบเท่าที่คลาสยังถูกโหลด (ใน Android — ตราบเท่าที่ Process ยังมีชีวิตอยู่) หากฟิลด์สแตติกอ้างอิงถึง Activity, Bitmap, View หรือวัตถุหนักอื่น ๆ วัตถุนั้นจะไม่มีวันถูกเก็บโดย GC ฟิลด์สแตติกคือการอ้างอิงนิรันดร์ วิธีแก้ไข: เก็บเฉพาะ WeakReference หรือทำให้ฟิลด์สแตติกเป็น null ใน onDestroy

จะหลีกเลี่ยงการรั่วไหลใน iOS ด้วย ARC ได้อย่างไร?

ARC ปล่อยวัตถุโดยอัตโนมัติเมื่อจำนวนการอ้างอิงแบบแข็งลดลงเป็นศูนย์ retain cycle เป็นวิธีเดียวที่จะรั่วไหลภายใต้ ARC ใช้ weak เสมอสำหรับการอ้างอิง parent→child ที่ child อาจมีอายุยืนกว่า parent (ตัวแทน, data source) สำหรับ closures ใช้รายการดักจับ [weak self] และตรวจสอบ self ว่าเป็น nil หรือไม่ภายใน closure

สรุป

  • หน่วยความจำรั่ว — วัตถุไม่สามารถเข้าถึงได้โดยโค้ดแต่ไม่ได้ถูกลบโดย GC เนื่องจากการอ้างอิงที่ทำงานอยู่จาก GC Root
  • GC Roots รวมถึงฟิลด์สแตติก ตัวแปรสแต็ก และการอ้างอิง JNI; วัตถุใด ๆ ที่เข้าถึงได้จากสิ่งเหล่านี้มีชีวิตอยู่
  • Context รั่ว — ปัญหาที่แพร่หลายที่สุดใน Android: การส่ง Activity Context ไปยังซิงเกิลตันหรือฟิลด์สแตติก
  • Handler และคลาสภายใน — สาเหตุที่พบบ่อยเป็นอันดับสอง: ข้อความที่ไม่ถูกยกเลิกในคิว Looper รักษาการอ้างอิงไปยัง Activity
  • LeakCanary — เครื่องมือตรวจจับอัตโนมัติมาตรฐาน; ทำ heap dump และแสดง GC root chain ที่แน่นอน
  • lifecycleScope และ viewModelScope แก้ปัญหาการรั่วไหลผ่าน coroutine — การยกเลิกอัตโนมัติเมื่อถูกทำลาย
  • สร้างโปรไฟล์หน่วยความจำ ใน CI/CD: LeakCanary ในโหมดดีบัก + การวิเคราะห์ heap dump ในการทดสอบควรบล็อกการรวมเมื่อมีการรั่วไหลใหม่

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม