การจัดการหน่วยความจำอัตโนมัติผ่านการเก็บขยะเป็นกลไกสำคัญของแพลตฟอร์ม Android ซึ่งทำงานบนเครื่องเสมือน ART ตาม Google Android Documentation, 2026 ตัวเก็บขยะช่วยให้นักพัฒนาหลุดพ้นจาก การจัดการหน่วยความจำ ด้วยตนเอง โดยลบอ็อบเจ็กต์ที่ไม่มีการอ้างอิงอีกต่อไปโดยอัตโนมัติ หากไม่มี GC การจัดสรรอ็อบเจ็กต์ทุกครั้งจะต้องเรียกใช้ free หรือ delete อย่างชัดเจน ซึ่งในระบบนิเวศ Java ที่มีอ็อบเจ็กต์นับล้านต่อวินาทีนั้นเป็นไปไม่ได้ทางกายภาพ
ประเด็นสำคัญ
Garbage Collection (GC) เป็นกระบวนการอัตโนมัติในการตรวจจับและเพิ่มหน่วยความจำที่ถูกครอบครองโดยอ็อบเจ็กต์ที่โปรแกรมไม่ได้ใช้อีกต่อไป ในบริบทของการพัฒนาแอปพลิเคชันมือถือ GC ถูกใช้บนแพลตฟอร์ม Android ผ่านเครื่องเสมือน ART รวมถึงใน Java Virtual Machine มาตรฐาน
แตกต่างจากภาษาที่จัดการหน่วยความจำด้วยตนเอง (C, C++) ซึ่งโปรแกรมเมอร์ต้องเรียกใช้ free หรือ delete อย่างชัดเจน GC จะรับหน้าที่ติดตามวงจรชีวิตของอ็อบเจ็กต์อย่างสมบูรณ์ นักพัฒนาสร้างอ็อบเจ็กต์ใหม่ผ่านตัวดำเนินการ new ในขณะที่ตัวเก็บจะกำหนดว่าเมื่อใดที่อ็อบเจ็กต์ไม่สามารถเข้าถึงได้ — กล่าวคือไม่มีการอ้างอิงที่ยังทำงานอยู่เหลืออยู่
เมตริกหลัก ของประสิทธิภาพ GC คือเวลาหยุดชั่วคราว (pause time) และปริมาณงาน (throughput) การหยุดชั่วคราวคือช่วงเวลาที่การทำงานของแอปพลิเคชันถูกหยุดเพื่อดำเนินการเก็บ ในสภาพแวดล้อมมือถือ การหยุดชั่วคราวที่นานกว่า 8–16 มิลลิวินาทีจะสังเกตเห็นได้เป็นเฟรมที่ตกหล่น (jank)
ตาม Google I/O 2019 ART ใน Android 10 ลดการหยุดชั่วคราว GC ทั่วไปลงเหลือ 2–4 ms ซึ่งน้อยกว่า Dalvik ใน Android 4.4 ถึง 70% อย่างไรก็ตาม การจัดการหน่วยความจำที่ไม่เหมาะสม — การจัดสรรอ็อบเจ็กต์บ่อยครั้งในลูป การสร้างอินสแตนซ์ชั่วคราวโดยไม่จำเป็น — ยังคงเป็นสาเหตุหลักของปัญหาด้านประสิทธิภาพ
การใช้งาน GC ทั้งหมดใน Java และ Android ขึ้นอยู่กับอัลกอริทึมพื้นฐานหลายอย่างที่ถูกรวมกันเพื่อให้เกิดความสมดุลระหว่างเวลาหยุดชั่วคราวและความสมบูรณ์ของการทำความสะอาด การทำความเข้าใจอัลกอริทึมเหล่านี้จำเป็นสำหรับการเขียน โค้ดที่เป็นมิตรกับ GC
Mark-and-Sweep เป็นอัลกอริทึมที่ง่ายที่สุด ทำงานในสองขั้นตอน ในขั้นตอน Mark ตัวเก็บจะ遍历กราฟอ็อบเจ็กต์โดยเริ่มจากการอ้างอิงราก (root set) — ตัวแปรท้องถิ่น ฟิลด์สแตติก สแต็กเธรด อ็อบเจ็กต์ที่เข้าถึงได้แต่ละอันจะถูกทำเครื่องหมายด้วยแฟล็กมีชีวิต ในขั้นตอน Sweep ตัวเก็บจะ遍历ฮีปทั้งหมดและเพิ่มหน่วยความจำของอ็อบเจ็กต์ที่ไม่ได้ถูกทำเครื่องหมาย
ข้อเสียคือการกระจายตัวของหน่วยความจำ: หลัง Sweep พื้นที่ว่างและพื้นที่ถูกครอบครองจะสลับกัน ทำให้การจัดสรรอ็อบเจ็กต์ขนาดใหญ่ทำได้ยาก ในสถานการณ์มือถือ สิ่งนี้สำคัญเนื่องจากฮีปมักจะมีขนาดเล็ก (64–512 MB บน Android)
Copying Collection แบ่งฮีปออกเป็นสองครึ่งพื้นที่ (semi-spaces) อ็อบเจ็กต์ที่ยังทำงานอยู่จะถูกคัดลอกอย่างกะทัดรัดจากครึ่งพื้นที่หนึ่งไปยังอีกครึ่งหนึ่ง โดยไม่มีช่องว่าง หลังจากการคัดลอก ครึ่งพื้นที่เก่าจะถูกประกาศว่าเป็นอิสระอย่างสมบูรณ์ อัลกอริทึมกำจัด การกระจายตัว ได้อย่างสมบูรณ์ แต่ต้องใช้หน่วยความจำเป็นสองเท่า
ในสภาพแวดล้อมมือถือ Copying Collection ถูกใช้โดยตัวเก็บแบบเจเนอเรชันเพื่อทำความสะอาดอ็อบเจ็กต์อายุน้อยอย่างรวดเร็ว ซึ่งในทางสถิติแล้วตายเร็ว (สมมติฐานเจเนอเรชันแบบอ่อน)
Generational Collection แบ่งฮีปออกเป็นเจเนอเรชัน: Young Generation (อ็อบเจ็กต์อายุน้อย) และ Old Generation (อ็อบเจ็กต์อายุมากที่รอดจากการเก็บหลายครั้ง) การเก็บเจเนอเรชันอายุน้อย (Minor GC) จะดำเนินการบ่อยครั้งและรวดเร็ว เนื่องจากอ็อบเจ็กต์ส่วนใหญ่ตายเมื่ออายุน้อย การเก็บเจเนอเรชันอายุมาก (Major GC หรือ Full GC) เกิดขึ้นน้อยกว่าแต่ใช้เวลานานกว่า
// การสาธิต GC แบบเจเนอเรชัน: อ็อบเจ็กต์อายุน้อยตายเร็ว
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // มีชีวิตตลอดทั้งเมธอด
for (Item item : items) {
Result r = new Result(item.getValue()); // ตายทันที
if (r.isValid()) {
process(r); // r กลายเป็นขยะ
}
}
saveResults(results); // results ย้ายไปยัง Old Gen
}
ในตัวอย่างนี้ อ็อบเจ็กต์ Result ถูกสร้างขึ้นภายในลูปและกลายเป็นขยะทันที — เหมาะอย่างยิ่งสำหรับ Young GC อ็อบเจ็กต์ results มีอายุยืนยาวกว่าและย้ายไปยัง Old Generation การแยกเจเนอเรชันช่วยให้ Minor GC ทำความสะอาดอ็อบเจ็กต์อายุน้อยในหน่วยมิลลิวินาทีโดยไม่แตะต้องฮีปเก่า
Android พัฒนาจาก Dalvik VM ไปสู่ ART (Android Runtime) และการใช้งาน GC เป็นหนึ่งในความแตกต่างหลักระหว่างทั้งสอง การทำความเข้าใจสถาปัตยกรรม GC ใน Android ช่วยเขียนโค้ดที่ ลดการหยุดชั่วคราว บนอุปกรณ์จริง
| คุณลักษณะ | Dalvik (จนถึง 4.4) | ART (5.0+) |
|---|---|---|
| ประเภท GC | Mark-and-Sweep พร้อม Concurrent Mark | Generational + Concurrent |
| การหยุดชั่วคราวทั่วไป | 10–30 ms | 2–4 ms |
| การบีบอัด | ไม่ (เฉพาะการกระจายตัวเพิ่มขึ้น) | ใช่ (ในพื้นหลัง โดยไม่หยุดแอป) |
| การคอมไพล์ AOT | JIT (Just-In-Time) | AOT + JIT (ไฮบริด) |
Dalvik ใช้การผสมผสานของ Mark-and-Sweep กับเฟสพร้อมกัน Concurrent Mark อนุญาตให้แอปพลิเคชันทำงานต่อไประหว่างการ遍历กราฟอ็อบเจ็กต์ แต่เฟส Sweep จำเป็นต้องหยุดเธรดทั้งหมด (Stop-The-World) บนอุปกรณ์ที่มี RAM น้อย (512 MB — 1 GB) การหยุดชั่วคราวถึง 30 ms ทำให้เกิด ความล่าช้าที่สังเกตเห็นได้ ในอินเทอร์เฟซ นอกจากนี้ Dalvik ไม่ได้บีบอัดฮีป ดังนั้นหลังจากการใช้งานเป็นเวลานาน การกระจายตัวเพิ่มขึ้น และการจัดสรรอ็อบเจ็กต์ขนาดใหญ่ (เช่น Bitmap) อาจโยน OutOfMemoryError แม้ว่าจะมีหน่วยความจำว่างทั้งหมดเพียงพอก็ตาม
ART (Android Runtime) นำเสนอตัวเก็บแบบเจเนอเรชันพร้อมการบีบอัดแบบพร้อมกัน ฮีปแบ่งออกเป็นสามภูมิภาค: Young, Mature (คล้ายกับ Old Generation) และ Large Object Space (สำหรับอ็อบเจ็กต์ที่ใหญ่กว่า 12 KB) การเก็บ Young Region เกิดขึ้นแบบขนานโดยไม่หยุดเธรดในกรณีส่วนใหญ่ ใน Android 10+ มีการนำเสนอ Concurrent Copying — การบีบอัดทำงานในเธรดพื้นหลังโดยไม่มี Stop-The-World
ด้วยสถาปัตยกรรมของ ART การหยุดชั่วคราว GC ทั่วไปลดลงเหลือ 2–4 ms และในสถานการณ์ที่มีอ็อบเจ็กต์อายุน้อยเป็นส่วนใหญ่ — เหลือ 0.5–1 ms ทำให้อุปกรณ์ Android สามารถรักษา 60 FPS ที่เสถียรได้แม้ในระหว่างการดำเนินการหน่วยความจำที่แอคทีฟ
ในระบบนิเวศ Java มีการใช้งาน GC หลายแบบ แต่ละแบบมีโปรไฟล์ประสิทธิภาพของตัวเอง สำหรับการพัฒนา Android ตัวเลือกจำกัดอยู่ที่ ART แต่ความรู้เกี่ยวกับ Java GC มีประโยชน์เมื่อเขียน โค้ดฝั่งเซิร์ฟเวอร์ สำหรับแอปพลิเคชันมือถือและเมื่อพัฒนาด้วย Kotlin Multiplatform
Serial GC เป็นตัวเก็บแบบเธรดเดียวที่มีการหยุดแอปพลิเคชันอย่างสมบูรณ์ (Stop-The-World) การดำเนินการ Mark, Sweep และ Compact แต่ละครั้งดำเนินการโดยหนึ่งเธรด ประสิทธิภาพต่ำ — ไม่ได้ใช้สำหรับเซิร์ฟเวอร์มือถือ เหมาะสำหรับแอปพลิเคชันขนาดเล็กที่มีฮีปสูงถึง 100 MB เท่านั้น
Parallel GC (หรือที่รู้จักในชื่อ Throughput Collector) ใช้หลายเธรดสำหรับทุกเฟสของการเก็บ มันมุ่งเน้นไปที่ปริมาณงานสูงสุด (throughput) — ลดเวลาที่ใช้ใน GC เมื่อเทียบกับเวลาทำงานของแอปพลิเคชัน เปิดใช้งานผ่านแฟล็ก -XX:+UseParallelGC ใน JVM
G1 (Garbage-First) GC เป็นตัวเก็บเริ่มต้นใน Java 9+ ฮีปถูกแบ่งออกเป็นภูมิภาคขนาด 1–32 MB G1 ทำนายเวลาหยุดชั่วคราวและพยายามให้อยู่ในขีดจำกัดที่กำหนด (ค่าเริ่มต้น 200 ms) ลำดับความสำคัญ: ภูมิภาคที่มีขยะมากที่สุดจะถูกทำความสะอาดก่อน (จึงเป็นที่มาของชื่อ) G1 มีประสิทธิภาพสำหรับ เซิร์ฟเวอร์ที่มีฮีปขนาดใหญ่ (4–64 GB) โดยมีการหยุดชั่วคราวที่คาดเดาได้
// การเปิดใช้งาน G1 GC โดยมีเป้าหมายการหยุดชั่วคราว 100 ms
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("Heap usage exceeded threshold: " + used);
System.out.println("Consider reducing allocations");
}
}
}
การตรวจสอบฮีปผ่าน Runtime ช่วยตรวจจับการรั่วไหลของหน่วยความจำในระยะเริ่มต้น หาก used เกิน 80% ของฮีปสูงสุดในการทำงานที่เสถียร — นี่คือสัญญาณของการรั่วไหลที่อาจเกิดขึ้นหรือ การใช้หน่วยความจำที่มากเกินไป โดยแอปพลิเคชัน
แม้แต่ ART GC ที่ทันสมัยก็ไม่สามารถแก้ปัญหาทั้งหมดได้ — การใช้หน่วยความจำที่ไม่เหมาะสมยังคงเป็นสาเหตุหลักของ jank และ ANR (Application Not Responding) มาดูสถานการณ์หลักและวิธีการปรับแต่งกัน
GC Pauses — การหยุดเธรดของแอปพลิเคชันระหว่างการเก็บ บนหน้าจอ สิ่งนี้ปรากฏเป็นเฟรมที่ตกหล่น เมื่อเวลาระหว่างสองเฟรมเกิน 16.6 ms (60 FPS) หาก GC ใช้เวลา 30 ms จะมีเพียงหนึ่งเฟรมที่ถูกวาดแทนที่จะเป็นสอง — ผู้ใช้เห็น การกระตุก ของอินเทอร์เฟซ
สาเหตุหลักของการหยุดชั่วคราวที่ยาวนาน: จำนวนมากของอ็อบเจ็กต์ที่มีชีวิตใน Old Generation การกระจายตัวของฮีป Full GC ที่บ่อยครั้ง สำหรับการวินิจฉัย ใช้ Android Studio Profiler และ systrace
กฎหลัก ของโค้ดที่เป็นมิตรกับ GC คือการลดจำนวนอ็อบเจ็กต์ที่ถูกจัดสรรให้เหลือน้อยที่สุด อ็อบเจ็กต์ใหม่แต่ละอันไม่เพียงต้องการการจัดสรรหน่วยความจำเท่านั้น แต่ยังต้องมีการเก็บในภายหลังอีกด้วย แม้ว่า GC จะเร็ว การจัดสรรเพิ่ม 1000 ครั้งต่อวินาทีก็สร้างการตรวจสอบ 1000 ครั้งให้กับตัวเก็บ
การรั่วไหลของหน่วยความจำ เกิดขึ้นเมื่ออ็อบเจ็กต์ยังคงสามารถเข้าถึงได้แม้ว่าจะไม่ต้องการอีกต่อไป GC ไม่สามารถลบอ็อบเจ็กต์ดังกล่าวได้และหน่วยความจำจะค่อยๆหมดไป สาเหตุทั่วไป: listener ที่ไม่ได้ยกเลิกการลงทะเบียน การอ้างอิงแบบสแตติกไปยัง Activity คลาสนิรนามที่จับบริบทภายนอก และ Cursor/InputStream ที่ไม่ได้ปิด
// การรั่วไหลของหน่วยความจำ: คลาสนิรนามถือการอ้างอิงไปยัง Activity
public void startTask() {
new Thread(new Runnable() { // ถือ this (Activity) โดยนัย
@Override
public void run() {
// การดำเนินการที่ยาวนาน...
System.out.println("Done");
}
}).start();
}
// การแก้ไข: คลาสซ้อนแบบสแตติก + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// การทำงานที่ปลอดภัยกับ Activity
}
}
}
ในตัวอย่างนี้ Runnable แบบนิรนามจับการอ้างอิงโดยนัยไปยัง Activity ตราบใดที่เธรดยังมีชีวิตอยู่ — Activity ไม่สามารถถูกเก็บโดย GC แม้ว่าผู้ใช้จะปิดหน้าจอไปแล้วก็ตาม การแก้ไขด้วย WeakReference + คลาสสแตติก ทำลายห่วงโซ่นี้และอนุญาตให้ Activity ถูกเพิ่มหน่วยความจำ
คำถามที่พบบ่อย
GC ใน Android (ART) เป็นตัวเก็บแบบเจเนอเรชันที่มีการบีบอัดแบบพร้อมกัน ปรับแต่งสำหรับอุปกรณ์มือถือที่มีหน่วยความจำจำกัด Java GC (G1, ZGC) เป็นตัวเก็บฝั่งเซิร์ฟเวอร์ที่มีฮีปขนาดใหญ่และการหยุดชั่วคราวที่คาดเดาได้ ART GC ไม่ใช้แฟล็ก JVM — การปรับแต่งทั้งหมดทำโดยอัตโนมัติที่ระดับ OS
Stop-The-World คือช่วงเวลาที่ตัวเก็บหยุดเธรดทั้งหมดของแอปพลิเคชันชั่วคราวเพื่อ遍历กราฟอ็อบเจ็กต์หรือเพิ่มหน่วยความจำอย่างปลอดภัย ยิ่ง STW นานเท่าไหร่ jank ก็ยิ่งสังเกตเห็นได้ชัดเจนมากขึ้น ART ลดเวลา STW ทั่วไปลงเหลือ 2–4 ms เนื่องจากสถาปัตยกรรมแบบเจเนอเรชัน
ใช้ Android Studio Memory Profiler — มันแสดงการเติบโตของฮีป จำนวนการจัดสรร และอนุญาตให้ทำ Heap Dump สำหรับการวิเคราะห์เชิงลึก ให้ใช้ LeakCanary — ไลบรารีตรวจจับการรั่วไหลโดยอัตโนมัติและแสดงห่วงโซ่การอ้างอิงที่ขัดขวางการเก็บ GC
Full GC คือการเก็บอย่างสมบูรณ์ของทุกเจเนอเรชันของฮีป รวมถึง Old Generation ในแอปพลิเคชันมือถือ Full GC อาจใช้เวลา 50–200 ms ทำให้เกิด jank หรือ ANR ที่สังเกตเห็นได้ สาเหตุหลัก: การกระจายตัวของฮีป การรั่วไหลของหน่วยความจำ การเกินขีดจำกัดของ Old Generation
Kotlin มี coroutine พร้อมการทำงานพร้อมกันแบบมีโครงสร้าง — การยกเลิกขอบเขตจะยกเลิก coroutine ลูกทั้งหมดโดยอัตโนมัติ ป้องกันการรั่วไหล Kotlin ยังมีตัวแทน lazy สำหรับการเริ่มต้นแบบขี้เกียจและฟังก์ชันขอบเขตที่ลดจำนวนอ็อบเจ็กต์ชั่วคราว
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม