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

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

หน่วยความจำรั่ว — หนึ่งในปัญหาที่ร้ายกาจที่สุดในการพัฒนาแอปมือถือ การใช้หน่วยความจำของแอปเพิ่มขึ้นเรื่อย ๆ จนกระทั่งถึงขีดจำกัดที่ระบบปฏิบัติการกำหนด ตามด้วย OutOfMemoryError หรือการยุติการทำงานโดยบังคับ จากข้อมูลของ Square Engineering ประมาณ 40% ของแอป Android มีหน่วยความจำรั่วอย่างน้อยหนึ่งจุดที่สามารถตรวจพบได้ผ่านการทำโปรไฟล์เท่านั้น มาดูสาเหตุและวิธีการป้องกันการเติบโตของหน่วยความจำกัน

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

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

หน่วยความจำรั่วและการบวมของแอปคืออะไร?

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

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

ใน Android แอปแต่ละตัวจะได้รับการจัดสรรฮีปแบบจำกัด (โดยปกติ 64–512 MB ขึ้นอยู่กับอุปกรณ์และเวอร์ชันระบบปฏิบัติการ) ใน iOS ขีดจำกัดเข้มงวดน้อยกว่า แต่ระบบจะส่งคำเตือนหน่วยความจำเมื่อเข้าใกล้ขีดจำกัด

คุณลักษณะAndroidiOS
ขีดจำกัดฮีป64–512 MB (ขึ้นอยู่กับอุปกรณ์)โดยนัย (ระบบ)
การเก็บขยะART (พร้อมกัน, ขนาดกะทัดรัด)ARC (การนับการอ้างอิงอัตโนมัติ)
กลไกการรั่วการอ้างอิง GC Rootวงจรการคงอยู่ (วงจรการอ้างอิงที่แข็งแกร่ง)
ผลลัพธ์OutOfMemoryErrorคำเตือนหน่วยความจำ → การยุติ

ตาม Facebook Engineering Blog หน่วยความจำรั่วเป็นสาเหตุของรายงานการขัดข้องประมาณ ~15% ในแอปมือถือ ใน Android ยังมี ANR เพิ่มเติมจากการหยุด GC บ่อยครั้งเมื่อหน่วยความจำเหลือน้อย

รูปแบบหน่วยความจำรั่วที่พบบ่อยใน Android และ iOS

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

kotlin
object LeakHolder {
    var activityRef: Activity ?= null // leak: static reference to Activity
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
    }
}

คลาสนิรนามและแลมบ์ดา — เก็บการอ้างอิงไปยังคลาสภายนอกโดยปริยาย หาก Runnable หรือ Callback ถูกส่งไปยังบริการภายนอกและ Activity ถูกทำลาย ออบเจ็กต์คลาสนิรนามยังคงอยู่ในคิวและป้องกันไม่ให้ Activity ถูกเก็บขยะ

  • Handler แบบหน่วงเวลา — หาก Activity ถูกทำลายแต่ Handler.postDelayed ยังไม่ได้ดำเนินการ Activity จะรั่ว
  • Thread และ AsyncTask — เมื่อหมุนหน้าจอ Activity ถูกสร้างใหม่ในขณะที่ Thread เก่ายังคงเก็บการอ้างอิงไปยัง Activity เก่า
  • Retrofit/Callback — Callback นิรนามเก็บการอ้างอิงไปยังพรีเซนเตอร์หรือแฟรกเมนต์
  • ผู้สังเกตการณ์ — การสมัครสมาชิก LiveData หรือ RxJava โดยไม่ยกเลิกที่ onDestroy

ใน iOS ปัญหาหลักคือ วงจรการคงอยู่: ออบเจ็กต์สองตัวเก็บการอ้างอิงที่แข็งแกร่งซึ่งกันและกัน และ ARC ไม่สามารถทำให้ตัวนับการอ้างอิงเป็นศูนย์สำหรับทั้งสองได้ กรณีทั่วไป: คลอเชอร์ที่จับ self อย่างแข็งแกร่ง และ self ที่เก็บการอ้างอิงไปยังคลอเชอร์

จะตรวจจับหน่วยความจำรั่วได้อย่างไร?

LeakCanary — ไลบรารีจาก Square สำหรับการตรวจจับการรั่วแบบอัตโนมัติใน Android หลังจาก Activity หรือ Fragment ถูกทำลาย มันจะตรวจสอบว่าออบเจ็กต์ถูกเก็บโดย GC หรือไม่ ถ้าไม่ มันจะทำฮีปดัมพ์และแสดงร่องรอยการรั่ว

kotlin
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // LeakCanary auto-installs in debug build
        // via ContentProvider — zero code setup
    }
}

// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")

Android Studio Profiler — เครื่องมือในตัวสำหรับการตรวจสอบหน่วยความจำแบบเรียลไทม์ ช่วยให้บันทึกฮีปดัมพ์ ค้นหาออบเจ็กต์ที่น่าสงสัย (Retained Size > 1 MB) และติดตามเส้นทางราก GC ไปยังแต่ละออบเจ็กต์

สำหรับ iOS ให้ใช้ Xcode Memory Graph Debugger มันแสดงภาพกราฟออบเจ็กต์ในหน่วยความจำ แสดงวงจรการคงอยู่ และช่วยให้ตรวจจับการอ้างอิงแบบวงกลมได้ทันที Instruments > Allocations ก็พร้อมใช้งานสำหรับการตรวจสอบระยะยาว

กลยุทธ์การป้องกัน

WeakReference — กลไกพื้นฐานสำหรับการอ้างอิงที่ไม่ควรรบกวนการเก็บขยะ หาก GC ตัดสินใจเก็บออบเจ็กต์ WeakReference จะคืนค่า null ใช้สำหรับการเรียกกลับ ตัวฟัง และการอ้างอิงถึงคอมโพเนนต์ UI จากเธรดพื้นหลัง

คอมโพเนนต์ที่รับรู้วงจรชีวิต — แนวทางสถาปัตยกรรมที่นำมาใช้ใน Android Jetpack (Lifecycle, LiveData, Flow, coroutines) การสมัครสมาชิกจะถูกยกเลิกโดยอัตโนมัติที่ onDestroy ซึ่งกำจัดคลาสหลักของการรั่ว

kotlin
class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<List<User>>()
    val data: LiveData<List<User>> get() = _data

    fun loadData() {
        viewModelScope.launch {
            val result = repository.fetchData()
            _data.postValue(result)
            // coroutine auto-cancels on onCleared()
        }
    }
}

viewModelScope และ lifecycleScope — CoroutineScope ในตัวใน Android ที่ถูกยกเลิกเมื่อเหตุการณ์วงจรชีวิตที่เกี่ยวข้อง ซึ่งกำจัดการรั่วผ่านโคโรทีน — สถานการณ์ที่พบบ่อยที่สุดในการพัฒนา Android สมัยใหม่

  • อย่าใช้ การอ้างอิงแบบคงที่ไปยัง Context, Activity, View หรือ Fragment
  • ยกเลิก การสมัครสมาชิก RxJava ทั้งหมดใน disposeBag / CompositeDisposable ที่ onDestroy
  • ใช้ [weak self] / [unowned self] ในคลอเชอร์ iOS เพื่อป้องกันวงจรการคงอยู่
  • ตรวจสอบ Bitmap และออบเจ็กต์ขนาดใหญ่ — ควรรีไซเคิลหรือทำให้เป็น null

เครื่องมือทำโปรไฟล์หน่วยความจำ

Memory Profiler ใน Android Studio — เครื่องมือหลักสำหรับการตรวจสอบฮีป แสดงการจัดสรรสด ภาพรวมฮีป และจำนวนออบเจ็กต์ตามประเภท ช่วยให้บันทึกดัมพ์และวิเคราะห์ใน MAT (Memory Analyzer Tool) เพื่อค้นหาออบเจ็กต์ที่น่าสงสัย

Eclipse MAT — โปรแกรมวิเคราะห์ฮีปดัมพ์บนเดสก์ท็อป หลังจากโหลดไฟล์ HPROF จาก Android Studio MAT จะสร้างโครงสร้างแบบโดมิเนเตอร์ แสดงขนาดการคงอยู่ของแต่ละออบเจ็กต์ และเสนอการวิเคราะห์การรั่วที่น่าสงสัยอัตโนมัติผ่าน Leak Suspects Report

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

เครื่องมือแพลตฟอร์มคุณสมบัติ
LeakCanaryAndroidตรวจจับการรั่วอัตโนมัติหลังถูกทำลาย
Memory ProfilerAndroid Studioฮีปดัมพ์ + การจัดสรรสด
Eclipse MATAndroidโครงสร้างแบบโดมิเนเตอร์, Leak Suspects Report
Memory GraphiOS (Xcode)ตัวแสดงภาพวงจรการคงอยู่

ตาม Google I/O 2023 แอปที่ใช้ LeakCanary ในบิลด์ดีบักลดการขัดข้องที่เกี่ยวข้องกับหน่วยความจำลง 30–50% ใน 2 เดือนแรกหลังการนำไปใช้ แนะนำให้เพิ่ม LeakCanary ในช่วงเริ่มต้นโครงงาน

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

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

การรั่ว — ออบเจ็กต์ที่โค้ดไม่สามารถเข้าถึงได้ แต่ไม่ถูกเก็บโดย GC เนื่องจากการอ้างอิงที่ยังทำงานอยู่ การบวม — แอปเก็บออบเจ็กต์ที่จำเป็นตามตรรกะแต่ในปริมาณที่มากเกินไป (เช่น แคช 50 MB ในแอปที่ทำงานอยู่ 80 MB) การบวมแก้ไขด้วยสถาปัตยกรรม การรั่วแก้ไขด้วยการจัดการการอ้างอิงที่ถูกต้อง

LeakCanary หาการรั่วได้อย่างไร?

LeakCanary ใช้ ObjectWatcher — หลังจาก onDestroy() ของ Activity มันจะสร้าง WeakReference ไปยัง Activity และเรียกใช้ GC หาก WeakReference ไม่ถูกล้างหลังจาก 5 วินาที LeakCanary จะทำฮีปดัมพ์ วิเคราะห์ห่วงโซ่การอ้างอิงที่สั้นที่สุดจาก GC Root ไปยังออบเจ็กต์ และแสดงสแต็กการรั่วที่แน่นอนพร้อมไฟล์และบรรทัดของโค้ด

ทำไม Bitmap มักทำให้เกิด OutOfMemoryError?

Bitmap ใช้หน่วยความจำภายนอกฮีป Java ในหน่วยความจำพื้นเมือง (native heap) ขนาดของ Bitmap หนึ่งอัน = ความกว้าง × ความสูง × 4 ไบต์ (ARGB_8888) รูปภาพ 12 MP (4000×3000) ใช้ 48 MB Android ไม่สามารถปล่อยหน่วยความจำพื้นเมืองได้ทันเวลาเสมอไป ดังนั้นการสะสมของ Bitmap หลายอันนำไปสู่ OOM แม้จะมีฮีป Java เพียงพอ

วงจรการคงอยู่ใน iOS คืออะไร?

วงจรการคงอยู่ — สถานการณ์ใน ARC ที่ออบเจ็กต์สองตัวเก็บการอ้างอิงที่แข็งแกร่งซึ่งกันและกัน และตัวนับการอ้างอิงไม่ถึงศูนย์ ตัวอย่างทั่วไป: ViewController ที่มีการอ้างอิงที่แข็งแกร่งไปยังคลอเชอร์ และคลอเชอร์จับ self อย่างแข็งแกร่ง วิธีแก้ไข: ใช้ [weak self] หรือ [unowned self] ในคลอเชอร์

ขนาดฮีปสูงสุดบน Android คือเท่าไร?

ขนาด ฮีป ขึ้นอยู่กับอุปกรณ์และเวอร์ชัน Android สำหรับอุปกรณ์เก่า (API 15–24) — 64–128 MB สำหรับอุปกรณ์สมัยใหม่ (API 25+) — 256–512 MB ค่าที่แน่นอนสามารถรับได้ผ่าน ActivityManager.getMemoryClass() สำหรับแอปขนาดใหญ่ (เกม โปรแกรมแก้ไข) largeHeap=true ในแมนิเฟสต์จะให้สูงสุด 1 GB

สรุป

  • หน่วยความจำรั่ว — ออบเจ็กต์ที่ GC ไม่ได้เก็บเนื่องจากการอ้างอิงที่ยังทำงานอยู่จากชุดราก; การบวม — การใช้หน่วยความจำมากเกินไปโดยไม่มีการรั่วที่ชัดเจน
  • การอ้างอิงแบบคงที่ ไปยัง Activity, Context หรือ View — สาเหตุอันดับหนึ่งของการรั่วใน Android; วิธีแก้ไข — WeakReference หรือ Application Context
  • คลาสนิรนาม และแลมบ์ดาเก็บการอ้างอิงไปยังคลาสภายนอกโดยปริยาย; การเรียกกลับที่ไม่ถูกยกเลิกเป็นสาเหตุที่พบบ่อยเป็นอันดับสอง
  • LeakCanary — มาตรฐานการตรวจจับการรั่วอัตโนมัติใน Android; การรวมใช้เวลา 5 นาทีและลดอัตราการขัดข้องลง 30–50%
  • lifecycleScope และ viewModelScope ยกเลิกโคโรทีนโดยอัตโนมัติเมื่อถูกทำลาย กำจัดการรั่วทั้งคลาส
  • วงจรการคงอยู่ใน iOS แก้ไขด้วย weak/unowned self ในคลอเชอร์และตัวแทน
  • ทำโปรไฟล์หน่วยความจำ อย่างน้อยหนึ่งครั้งต่อสปรินต์ — ฮีปดัมพ์ด้วย MAT หรือ Memory Graph ควรเป็นส่วนหนึ่งของการตรวจสอบโค้ด

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

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

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

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