หน่วยความจำรั่วในแอปพลิเคชันมือถือ — มันคืออะไร สาเหตุ และวิธีการตรวจจับ

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

หน่วยความจำรั่ว (Memory Leak) คือสถานการณ์ที่แอปพลิเคชันเก็บการอ้างอิงไปยังออบเจ็กต์ที่ไม่ได้ต้องการอีกต่อไป ทำให้ตัวเก็บขยะ (GC) ไม่สามารถปลดปล่อยหน่วยความจำที่ถูกครอบครองได้ ตามข้อมูลของ LeakCanary แม้ในแอปพลิเคชันที่เขียนอย่างดีก็พบการรั่ว 3–5 ครั้งต่อ 10,000 บรรทัดของโค้ด แต่ละครั้ง ที่รั่วจะค่อยๆ ลดหน่วยความจำที่พร้อมใช้งาน นำไปสู่ความช้าและ OutOfMemoryError

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

  • Memory Leak — ออบเจ็กต์ยังคงอยู่ในหน่วยความจำแม้ไม่มีการอ้างอิงที่ทำงานอยู่จากตรรกะของแอปพลิเคชัน
  • การอ้างอิงแบบสแตติก ไปยัง Activity หรือ Context — สาเหตุที่พบบ่อยที่สุดของการรั่วใน Android
  • LeakCanary — เครื่องมือมาตรฐานสำหรับการตรวจจับการรั่วโดยอัตโนมัติใน Android
  • WeakReference และ Application Context — เทคนิคพื้นฐานในการป้องกันการรั่ว
  • คอมโพเนนต์ Lifecycle-aware กำจัดการรั่วทั้งคลาสที่เกี่ยวข้องกับการสมัครรับข้อมูล

หน่วยความจำรั่วคืออะไร

หน่วยความจำรั่ว (Memory Leak) คือสถานการณ์ที่ออบเจ็กต์ยังคงสามารถเข้าถึงได้ผ่านห่วงโซ่ของการอ้างอิงแบบแข็ง (Strong Reference) แม้ว่ามันจะไม่จำเป็นต่อแอปพลิเคชันในเชิงตรรกะอีกต่อไป ตัวเก็บขยะ (GC) ถือว่าออบเจ็กต์ดังกล่าวยังมีชีวิตและไม่ปลดปล่อยหน่วยความจำที่มันครอบครอง ส่งผลให้หน่วยความจำฮีปที่พร้อมใช้งานลดลงอย่างต่อเนื่องและความถี่ของการหยุด GC เพิ่มขึ้น

แตกต่างจาก ภาษาที่มีการจัดการหน่วยความจำด้วยตนเอง (C, C++) ใน Java/Kotlin การรั่วไม่ใช่ free() ที่ถูกลืม แต่เป็นการอ้างอิงที่ถูกลืม ตราบใดที่มี strong reference จาก GC Root ไปยังออบเจ็กต์ที่รั่ว GC จะถือว่ามันจำเป็น GC Root ทั่วไป: ฟิลด์สแตติก, เธรดที่ทำงานอยู่, สแต็กการเรียก, การอ้างอิงทั่วโลก JNI

อันตราย ของการรั่วคือผลสะสม การรั่ว 100 KB หนึ่งครั้งไม่สังเกตเห็น แต่การรั่ว 100 ครั้งครอบครอง 10 MB และแอปพลิเคชันเริ่มช้าลงเนื่องจาก GC บ่อยครั้ง มวลวิกฤตของการรั่วนำไปสู่ OutOfMemoryError และแอปพลิเคชันขัดข้อง อาการของการรั่ว: การเติบโตของการใช้หน่วยความจำอย่างต่อเนื่องบนกราฟ Profiler, การหยุด GC บ่อยครั้งด้วย STW (Stop The World) และประสิทธิภาพ UI ที่ลดลง

ประเภทการรั่วที่พบบ่อยในแอปพลิเคชันมือถือ

ห้าประเภท ของการรั่วครอบคลุม 95% ของกรณีในการพัฒนาแอปมือถือ แต่ละประเภทมีสาเหตุและรูปแบบโค้ดเฉพาะของตัวเอง

การอ้างอิงแบบสแตติกไปยัง Activity หรือ Context

การรั่ว ที่รู้จักกันดีที่สุดใน Android คือการเก็บการอ้างอิงแบบสแตติกไปยัง Activity หรือ Context โค้ดทั่วไป: ฟิลด์ Activity แบบสแตติกที่ไม่ถูกทำให้เป็น null ใน onDestroy() ตราบใดที่ฟิลด์สแตติกยังมีชีวิต ทั้ง Activity ที่มีทรี View ซึ่งอาจครอบครอง 1–10 MB ก็ยังมีชีวิตอยู่ นี่คือการรั่วแบบคลาสสิกที่ LeakCanary พบเป็นอันดับแรก

วิธีแก้: อย่าเก็บ Activity หรือ Context ในฟิลด์สแตติกเด็ดขาด ใช้ Application Context สำหรับซิงเกิลตันที่มีอายุยืนกว่า Activity หากต้องการการอ้างอิงไปยัง Activity ให้ใช้ WeakReference<Activity>

kotlin
object MySingleton {
    private var weakActivity: WeakReference<Activity>? = null

    fun attach(activity: Activity) {
        weakActivity = WeakReference(activity)
    }
}

คลาสภายในที่มีการอ้างอิงโดยนัย

คลาส นิรนาม และคลาสภายในที่ไม่ใช่สแตติกเก็บการอ้างอิงไปยังคลาสที่ครอบครองโดยนัย Runnable ที่ส่งไปยัง Handler ซึ่งทำงานหลังจาก onDestroy() จะยึด Activity ทั้งตัว callback Retrofit ที่จับ Activity ก็ทำเช่นเดียวกัน นี่เป็นการรั่วที่ร้ายกาจที่สุด — การอ้างอิงโดยนัยไม่ปรากฏในโค้ด

นิพจน์ object และแลมบ์ดาใน Kotlin ก็จับการอ้างอิงไปยังคลาสภายนอกเช่นกัน ทำให้คลาสภายในเป็นสแตติก (หรือระดับบนสุดใน Kotlin) และส่งการอ้างอิงภายนอกผ่าน WeakReference สำหรับแลมบ์ดา ให้ใช้แนวทาง Lifecycle-aware กับ viewLifecycleOwner

ผู้ฟังและการสมัครรับข้อมูลที่ไม่ได้ยกเลิก

การสมัคร รับบริการระบบโดยไม่ยกเลิกเป็นการรั่วโดยตรง SensorManager, LocationManager, NotificationListener ที่ลงทะเบียนใน onResume() โดยไม่เรียก unregister ใน onPause() จะยึด Activity เช่นเดียวกัน: RxJava Disposable ที่ไม่ได้เพิ่มใน CompositeDisposable และ coroutine ที่เริ่มผ่าน GlobalScope

ใช้คอมโพเนนต์ Lifecycle-aware: observe() กับ LifecycleOwner จะยกเลิกการสมัครโดยอัตโนมัติเมื่อ onDestroy() สำหรับ RxJava — viewLifecycleOwner.lifecycle.addObserver กับ DisposableObserver สำหรับ coroutine — lifecycleScope.launch() ผูกกับวงจรชีวิต

kotlin
// การยกเลิกการสมัครอัตโนมัติผ่าน Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
    updateUI(data)
}

// coroutine กับ lifecycleScope
lifecycleScope.launch {
    viewModel.loadData().collect { render(it) }
}

Bitmap ที่ไม่มี recycle

Bitmap ครอบครองหน่วยความจำฮีปจำนวนมาก: หนึ่งบิตแมป FullHD คือ 1920 × 1080 × 4 ไบต์ = 8.3 MB หากสร้าง Bitmap สำหรับแต่ละรายการในรายการและไม่เรียก recycle() เมื่อซ่อน หน่วยความจำจะหมดอย่างรวดเร็ว ใน Android เวอร์ชันเก่า (ก่อน 3.0) Bitmap ถูกเก็บในหน่วยความจำเนทีฟ แต่ในเวอร์ชันสมัยใหม่อยู่ในฮีป Dalvik/ART และ GC สามารถปลดปล่อยได้ก็ต่อเมื่อไม่มี strong reference

ใช้ Glide หรือ Coil สำหรับโหลดรูปภาพ — ไลบรารีเหล่านี้จัดการแคชและการรีไซเคิลโดยอัตโนมัติ หากทำงานกับ Bitmap โดยตรง ให้เรียก bitmap.recycle() สำหรับรูปภาพขนาดใหญ่ที่ไม่ได้แสดงอีกต่อไป และใช้ inSampleSize เพื่อโหลดสำเนาที่ลดขนาด

การอ้างอิง Fragment หลังจาก onDestroyView

Fragment มีสองวงจรชีวิต: ของ Fragment เองและของ View หลังจาก onDestroyView() ทรี View ถูกทำลาย แต่ Fragment เองอาจยังคงอยู่ในหน่วยความจำหากมีการอ้างอิงภายนอก ข้อผิดพลาดทั่วไปคือการเก็บการอ้างอิงไปยัง Fragment ในอะแดปเตอร์ ViewPager หรือในกราฟการนำทางที่ไม่ถูกล้างเมื่อถูกทำลาย

อย่าเก็บ การอ้างอิงไปยัง Fragment ในฟิลด์ของออบเจ็กต์ที่มีอายุยืนนาน ใช้ childFragmentManager สำหรับ fragment ที่ซ้อนกันและ observe() กับ LifecycleOwner สำหรับการถ่ายโอนข้อมูลระหว่างกัน ViewPager2 แก้ปัญหานี้ในระดับ API: FragmentTransactionAdapter จัดการวงจรชีวิตอย่างถูกต้อง

วิธีตรวจจับหน่วยความจำรั่ว

การตรวจจับ การรั่วต้องตรวจสอบสองข้อเท็จจริง: หน่วยความจำไม่กลับมาหลังจากอายุขัยที่คาดหวังและจำนวนออบเจ็กต์บางประเภทเพิ่มขึ้นโดยไม่ลดลง กระบวนการวินิจฉัยประกอบด้วยสามขั้นตอน

ขั้นตอนแรก — การตรวจสอบด้วยภาพผ่าน Memory Profiler ใน Android Studio เปิดแท็บ Memory ดำเนินการเป้าหมาย (เปิดและปิดหน้าจอ) กด GC (Garbage Collection) และดูว่าหน่วยความจำกลับสู่ระดับเริ่มต้นหรือไม่ หากหลังจาก 3–4 รอบการเปิด-ปิด หน่วยความจำเพิ่มขึ้นอย่างต่อเนื่อง — มีการรั่ว

ขั้นตอนที่สอง — การทำ Heap Dump ใน Memory Profiler กด Dump Java Heap เปิดไฟล์ .hprof ที่ได้ใน Android Studio: คุณจะเห็นออบเจ็กต์ทั้งหมดในฮีปพร้อมขนาดและการอ้างอิง ค้นหาคลาสที่จำนวนควรเป็นศูนย์หลังจากปิดหน้าจอ ตัวอย่างเช่น MainActivity ที่มีจำนวน 2 หลังจากปิด — เป็นการรั่วที่ชัดเจน

ขั้นตอนที่สาม — การวิเคราะห์ Retained Size และ GC Root ใน Android Studio วิเคราะห์ Retained Size: หน่วยความจำเท่าใดจะถูกปลดปล่อยหากคุณลบออบเจ็กต์นี้ เส้นทางจาก GC Root ไปยังออบเจ็กต์แสดงสิ่งที่ยึดมันไว้: Static field → HashMap → Activity — และคุณเห็นจุดที่รั่ว แผง Reference แสดงผู้ถือออบเจ็กต์ทั้งหมด

เครื่องมือค้นหาการรั่ว

สี่เครื่องมือ ครอบคลุมการค้นหาการรั่วตั้งแต่การตรวจจับอัตโนมัติจนถึงการวิเคราะห์ Heap Dump เชิงลึก

เครื่องมือวิธีการรูปแบบผลลัพธ์
LeakCanaryการตรวจสอบอัตโนมัติHeap Dump + stack trace การรั่ว
Android Memory Profilerการตรวจสอบด้วยตนเองกราฟหน่วยความจำ + Heap Dump
MAT (Eclipse)การวิเคราะห์เชิงลึกรายงาน Dominator Tree + เส้นทาง GC Root
Perfettoการติดตามทั้งระบบไทม์ไลน์ + หน่วยความจำเนทีฟ

LeakCanary เป็นสิ่งที่ต้องมีสำหรับโปรเจกต์ Android ใดๆ มันตรวจจับการรั่วโดยอัตโนมัติเมื่อสิ้นสุดวงจรชีวิต Activity/Fragment และแสดงตำแหน่งการรั่วที่แน่นอนพร้อม stack trace การผสานรวม: หนึ่งบรรทัดใน build.gradle LeakCanary 2.x ไม่ต้องการการเริ่มต้นด้วยตนเอง — มันลงทะเบียน Application Watcher โดยอัตโนมัติ

วิธีป้องกันหน่วยความจำรั่ว

การป้องกัน การรั่วถูกสร้างไว้ในกระบวนการพัฒนาผ่านชุดกฎและเครื่องมือที่ตรวจสอบโค้ดในทุกขั้นตอน

กฎการอ้างอิงแบบแข็ง

อย่าเก็บการอ้างอิงไปยัง Activity, Fragment หรือ View เด็ดขาด ในฟิลด์สแตติก, ซิงเกิลตัน หรือออบเจ็กต์ที่มีอายุยืนนาน หากเลี่ยงการอ้างอิงไม่ได้ ให้ใช้ WeakReference หรือเก็บข้อมูลผ่าน ViewModel ซึ่งมีอายุตรงตามที่ต้องการและไม่ยึด View โดยตรง

สถาปัตยกรรม Lifecycle-aware

ViewModel และ LiveData จาก Android Architecture Components แก้ปัญหาวงจรชีวิตในระดับสถาปัตยกรรม ViewModel อยู่รอดจากการหมุนหน้าจอและไม่มีการอ้างอิงไปยัง View LiveData ยกเลิกการสมัคร observer โดยอัตโนมัติเมื่อ onDestroy() ใช้สิ่งเหล่านี้แทนการสมัครรับบริการระบบด้วยตนเอง

Code Review ที่เน้น GC Root

ในการตรวจสอบโค้ด ให้ใส่ใจกับ: ฟิลด์สแตติกที่มีประเภท Context/View, คลาสนิรนาม, แลมบ์ดาที่จับ Activity, การสมัครด้วยตนเอง, RxJava disposable ที่ไม่มี composite, การเก็บ Fragment ผ่าน Bundle ใน Kotlin ให้ตรวจสอบ coroutine เพิ่มเติมด้วย launch โดยไม่มีการผูกวงจรชีวิต

การตรวจสอบอัตโนมัติใน CI

LeakCanary สามารถทำงานเป็นส่วนหนึ่งของไปป์ไลน์ทดสอบ: รันการทดสอบการยอมรับกับ LeakCanary และทำให้ build ล้มเหลวหากพบการรั่ว สิ่งนี้ป้องกันไม่ให้การรั่วถึงระบบการผลิต เสริมการตรวจสอบด้วยกฎ StaticFieldLeak ของ Android Lint — มันค้นหาการรั่วที่อาจเกิดขึ้นในระดับการวิเคราะห์แบบสแตติก

kotlin
// LeakCanary ในการทดสอบ
class LeakTest {
    @Test
    fun activityShouldNotLeak() {
        ActivityScenario.launch(MainActivity::class.java)
            .close()
        LeakAssertions.assertNoLeak() // ล้มเหลวหากมีการรั่ว
    }
}

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

หน่วยความจำรั่วต่างจาก OutOfMemoryError อย่างไร?

การรั่ว เป็นสาเหตุ และ OutOfMemoryError เป็นผลลัพธ์ การรั่วครั้งเดียวไม่นำไปสู่ OOM แต่การสะสมของการรั่วหลายสิบครั้งทำให้ฮีปหมด OOM เป็นข้อยกเว้นร้ายแรง ในขณะที่การรั่วเป็นรูปแบบที่นำไปสู่มันเมื่อเวลาผ่านไป

วิธีค้นหาการรั่วโดยไม่ใช้ LeakCanary?

ผ่าน Android Memory Profiler: เปิดและปิดหน้าจอ 5 ครั้ง หลังจากปิดแต่ละครั้งให้เรียก GC หากหน่วยความจำไม่กลับสู่ระดับฐาน — มีการรั่ว ทำ Heap Dump และค้นหาคลาส Activity ที่จำนวนมากกว่า 0 หลังจากปิดในรายการ

Kotlin สามารถป้องกันการรั่วในระดับภาษาได้หรือไม่?

บางส่วน Kotlin แก้ปัญหา null-safety แต่ไม่จัดการ strong references coroutine กับ lifecycleScope และ viewModelScope ป้องกันการรั่วจากงานพื้นหลัง ในขณะที่ sealed class และ data class ลดจำนวนสถานะที่นำไปสู่การรั่ว การป้องกันหลักคือรูปแบบสถาปัตยกรรม ไม่ใช่คุณสมบัติภาษา

ทำไม LeakCanary พบการรั่วที่ไม่มีอยู่?

LeakCanary บางครั้งให้ผลบวกปลอม: ออบเจ็กต์อาจถูกระบบยึดไว้ชั่วคราว (เช่น InputMethodManager ยึด View ล่าสุด) ตรวจสอบด้วยตนเอง: ถ้า Retained Size < 1 KB และ GC Root เป็นบริการระบบ แสดงว่าอาจเป็นผลบวกปลอม

หน่วยความจำรั่วเกิดขึ้นบน Android เท่านั้นหรือ?

ไม่ การรั่วเกิดขึ้นได้บนทุกแพลตฟอร์มที่มี GC: iOS (Swift/Objective-C), Flutter (Dart), เว็บเบราว์เซอร์ (JavaScript) กลไกเหมือนกัน — strong reference จาก GC Root บน iOS ARC จัดการหน่วยความจำโดยอัตโนมัติ แต่ retain cycle ระหว่างออบเจ็กต์สร้างการรั่วแบบเดียวกัน

สรุป

  • Memory Leak — ออบเจ็กต์ที่ GC ไม่สามารถปลดปล่อยได้เนื่องจาก strong reference ที่ถูกลืม
  • การอ้างอิงแบบสแตติก ไปยัง Activity และ Context — สาเหตุที่พบบ่อยที่สุดของการรั่ว
  • การอ้างอิงโดยนัย ผ่านคลาสนิรนาม แลมบ์ดา และการสมัคร RxJava ร้ายกาจกว่าการอ้างอิงโดยชัดแจ้ง
  • LeakCanary ค้นหาการรั่วโดยอัตโนมัติและแสดง stack trace ที่แน่นอน
  • คอมโพเนนต์ Lifecycle-aware (ViewModel, LiveData, lifecycleScope) กำจัดการรั่วทั้งคลาส
  • Heap Dump และการวิเคราะห์ Retained Size — วิธีการหลักของการวินิจฉัยด้วยตนเอง
  • การป้องกัน รวมถึงการตรวจสอบโค้ดที่เน้น strong reference และการตรวจสอบ CI กับ LeakCanary

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

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

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

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