Quản lý bộ nhớ tự động thông qua thu gom rác là cơ chế chính của nền tảng Android, dựa trên máy ảo ART. Theo Google Android Documentation, 2026, trình thu gom rác giải phóng nhà phát triển khỏi quản lý bộ nhớ thủ công, tự động xóa các đối tượng không còn tham chiếu. Nếu không có GC, mỗi lần cấp phát đối tượng sẽ yêu cầu gọi free hoặc delete một cách tường minh, điều này là bất khả thi trong hệ sinh thái Java với hàng triệu đối tượng mỗi giây.
Những điểm chính
Garbage Collection (GC) là quá trình tự động phát hiện và giải phóng bộ nhớ bị chiếm giữ bởi các đối tượng mà chương trình không còn sử dụng nữa. Trong bối cảnh phát triển di động, GC được sử dụng trên nền tảng Android thông qua máy ảo ART, cũng như trong Java Virtual Machine tiêu chuẩn.
Không giống như các ngôn ngữ quản lý bộ nhớ thủ công (C, C++), nơi lập trình viên phải gọi free hoặc delete một cách tường minh, GC hoàn toàn đảm nhận nhiệm vụ theo dõi vòng đời của đối tượng. Nhà phát triển tạo đối tượng mới thông qua toán tử new, trong khi trình thu gom xác định khi nào một đối tượng trở nên không thể truy cập — nghĩa là không còn tham chiếu hoạt động nào đến nó.
Các chỉ số chính về hiệu quả của GC là thời gian tạm dừng (pause time) và thông lượng (throughput). Tạm dừng là khoảng thời gian thực thi ứng dụng bị dừng để tiến hành thu gom. Trong môi trường di động, thời gian tạm dừng lâu hơn 8–16 mili giây có thể nhận thấy dưới dạng khung hình bị rớt (jank).
Theo Google I/O 2019, ART trong Android 10 đã giảm thời gian tạm dừng GC điển hình xuống 2–4 ms, giảm 70% so với Dalvik trong Android 4.4. Tuy nhiên, quản lý bộ nhớ không đúng cách — cấp phát đối tượng thường xuyên trong vòng lặp, tạo các thể hiện tạm thời không cần thiết — vẫn là nguyên nhân chính gây ra vấn đề hiệu suất.
Tất cả các triển khai GC trong Java và Android đều dựa trên một số thuật toán cơ bản được kết hợp để đạt được sự cân bằng giữa thời gian tạm dừng và mức độ triệt để của việc dọn dẹp. Hiểu các thuật toán này là cần thiết để viết mã thân thiện với GC.
Mark-and-Sweep là thuật toán đơn giản nhất, hoạt động theo hai giai đoạn. Trong giai đoạn Mark, trình thu gom duyệt qua đồ thị đối tượng bắt đầu từ các tham chiếu gốc (root set) — biến cục bộ, trường tĩnh, ngăn xếp luồng. Mỗi đối tượng có thể truy cập được đánh dấu cờ sống. Trong giai đoạn Sweep, trình thu gom duyệt qua toàn bộ heap và giải phóng bộ nhớ của các đối tượng không được đánh dấu.
Nhược điểm là sự phân mảnh bộ nhớ: sau Sweep, các vùng trống xen kẽ với các vùng bị chiếm dụng, gây khó khăn cho việc cấp phát các đối tượng lớn. Trong các kịch bản di động, điều này rất quan trọng vì heap thường nhỏ (64–512 MB trên Android).
Copying Collection chia heap thành hai nửa không gian (semi-spaces). Các đối tượng hoạt động được sao chép nhỏ gọn từ nửa không gian này sang nửa không gian kia, không có khoảng trống. Sau khi sao chép, nửa không gian cũ được tuyên bố là trống hoàn toàn. Thuật toán loại bỏ hoàn toàn sự phân mảnh, nhưng yêu cầu gấp đôi bộ nhớ.
Trong môi trường di động, Copying Collection được các trình thu gom thế hệ sử dụng để dọn dẹp nhanh các đối tượng trẻ, vốn theo thống kê sẽ chết sớm (giả thuyết thế hệ yếu).
Generational Collection chia heap thành các thế hệ: Young Generation (đối tượng trẻ) và Old Generation (đối tượng già đã sống sót qua nhiều lần thu gom). Việc thu gom thế hệ trẻ (Minor GC) được thực hiện thường xuyên và nhanh chóng, vì hầu hết các đối tượng đều chết trẻ. Việc thu gom thế hệ già (Major GC hoặc Full GC) xảy ra ít thường xuyên hơn nhưng mất nhiều thời gian hơn.
// Minh họa GC thế hệ: các đối tượng trẻ chết nhanh
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // sống trong suốt phương thức
for (Item item : items) {
Result r = new Result(item.getValue()); // chết ngay lập tức
if (r.isValid()) {
process(r); // r trở thành rác
}
}
saveResults(results); // results chuyển sang Old Gen
}
Trong ví dụ này, các đối tượng Result được tạo bên trong vòng lặp và ngay lập tức trở thành rác — chúng là ứng viên lý tưởng cho Young GC. Đối tượng results sống lâu hơn và di chuyển đến Old Generation. Việc phân chia thế hệ cho phép Minor GC dọn dẹp các đối tượng trẻ trong mili giây mà không chạm vào heap cũ.
Android đã phát triển từ Dalvik VM đến ART (Android Runtime), và việc triển khai GC là một trong những khác biệt chính giữa chúng. Hiểu kiến trúc GC trong Android giúp viết mã giảm thiểu thời gian tạm dừng trên các thiết bị thực.
| Đặc điểm | Dalvik (đến 4.4) | ART (5.0+) |
|---|---|---|
| Loại GC | Mark-and-Sweep với Concurrent Mark | Generational + Concurrent |
| Thời gian tạm dừng điển hình | 10–30 ms | 2–4 ms |
| Nén (Compaction) | Không (chỉ phân mảnh tăng) | Có (trong nền, không dừng ứng dụng) |
| Biên dịch AOT | JIT (Just-In-Time) | AOT + JIT (kết hợp) |
Dalvik sử dụng kết hợp Mark-and-Sweep với một giai đoạn đồng thời. Concurrent Mark cho phép ứng dụng tiếp tục hoạt động trong quá trình duyệt đồ thị đối tượng, nhưng giai đoạn Sweep yêu cầu dừng tất cả các luồng (Stop-The-World). Trên các thiết bị có RAM nhỏ (512 MB — 1 GB), thời gian tạm dừng lên tới 30 ms, gây ra độ trễ đáng kể cho giao diện. Ngoài ra, Dalvik không nén heap, vì vậy sau khi sử dụng kéo dài, sự phân mảnh tăng lên và việc cấp phát các đối tượng lớn (ví dụ: Bitmap) có thể ném OutOfMemoryError ngay cả khi có đủ bộ nhớ trống tổng thể.
ART (Android Runtime) đã giới thiệu một trình thu gom thế hệ với tính năng nén đồng thời. Heap được chia thành ba vùng: Young, Mature (tương tự Old Generation) và Large Object Space (cho các đối tượng lớn hơn 12 KB). Việc thu gom Young Region diễn ra song song mà không dừng các luồng trong hầu hết các trường hợp. Trong Android 10+, Concurrent Copying đã được giới thiệu — quá trình nén chạy trong luồng nền mà không cần Stop-The-World.
Nhờ kiến trúc của ART, thời gian tạm dừng GC điển hình đã giảm xuống còn 2–4 ms và trong các kịch bản có nhiều đối tượng trẻ — xuống còn 0.5–1 ms. Điều này cho phép các thiết bị Android duy trì 60 FPS ổn định ngay cả trong các hoạt động bộ nhớ tích cực.
Trong hệ sinh thái Java, có một số triển khai GC, mỗi loại có cấu hình hiệu suất riêng. Đối với phát triển Android, sự lựa chọn bị giới hạn ở ART, nhưng kiến thức về Java GC rất hữu ích khi viết mã phía máy chủ cho các ứng dụng di động và khi phát triển với Kotlin Multiplatform.
Serial GC là trình thu gom đơn luồng với việc dừng hoàn toàn ứng dụng (Stop-The-World). Mỗi thao tác Mark, Sweep và Compact được thực hiện bởi một luồng. Hiệu suất thấp — không được sử dụng cho máy chủ di động. Chỉ phù hợp cho các ứng dụng nhỏ với heap lên đến 100 MB.
Parallel GC (còn được gọi là Throughput Collector) sử dụng nhiều luồng cho tất cả các giai đoạn thu gom. Nó hướng đến thông lượng tối đa (throughput) — giảm thiểu thời gian dành cho GC so với thời gian chạy ứng dụng. Được kích hoạt qua cờ -XX:+UseParallelGC trong JVM.
G1 (Garbage-First) GC là trình thu gom mặc định trong Java 9+. Heap được chia thành các vùng 1–32 MB. G1 dự đoán thời gian tạm dừng và cố gắng duy trì trong giới hạn đã chỉ định (mặc định 200 ms). Ưu tiên: các vùng có nhiều rác nhất được dọn dẹp trước (do đó có tên). G1 hiệu quả cho máy chủ có heap lớn (4–64 GB) với thời gian tạm dừng có thể dự đoán.
// Kích hoạt G1 GC với thời gian tạm dừng mục tiêu 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");
}
}
}
Giám sát heap thông qua Runtime cho phép phát hiện rò rỉ bộ nhớ ở giai đoạn sớm. Nếu used vượt quá 80% heap tối đa trong hoạt động ổn định — đây là tín hiệu của rò rỉ tiềm ẩn hoặc tiêu thụ bộ nhớ quá mức của ứng dụng.
Ngay cả ART GC hiện đại cũng không giải quyết được tất cả vấn đề — sử dụng bộ nhớ không đúng cách vẫn là nguyên nhân chính gây ra jank và ANR (Application Not Responding). Hãy xem xét các kịch bản chính và phương pháp tối ưu hóa.
GC Pauses — các luồng ứng dụng bị dừng trong quá trình thu gom. Trên màn hình, điều này biểu hiện dưới dạng khung hình bị rớt, khi thời gian giữa hai khung hình vượt quá 16.6 ms (60 FPS). Nếu GC kéo dài 30 ms, chỉ một khung hình được vẽ thay vì hai — người dùng thấy giật lag ở giao diện.
Nguyên nhân chính của thời gian tạm dừng dài: số lượng lớn đối tượng sống trong Old Generation, phân mảnh heap, Full GC thường xuyên. Để chẩn đoán, sử dụng Android Studio Profiler và systrace.
Quy tắc chính của mã thân thiện với GC là giảm thiểu số lượng đối tượng được cấp phát. Mỗi đối tượng mới không chỉ yêu cầu cấp phát bộ nhớ mà còn cả việc thu gom sau đó. Ngay cả khi GC nhanh, 1000 lần cấp phát thêm mỗi giây sẽ tạo ra 1000 lần kiểm tra cho trình thu gom.
Rò rỉ bộ nhớ xảy ra khi một đối tượng vẫn có thể truy cập được mặc dù không còn cần thiết. GC không thể xóa đối tượng như vậy và bộ nhớ dần cạn kiệt. Nguyên nhân điển hình: người nghe chưa được hủy đăng ký, tham chiếu tĩnh đến Activity, lớp ẩn danh nắm giữ ngữ cảnh bên ngoài và Cursor/InputStream chưa đóng.
// Rò rỉ bộ nhớ: lớp ẩn danh giữ tham chiếu đến Activity
public void startTask() {
new Thread(new Runnable() { // ngầm giữ this (Activity)
@Override
public void run() {
// thao tác dài...
System.out.println("Done");
}
}).start();
}
// Khắc phục: lớp tĩnh lồng nhau + 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) {
// làm việc an toàn với Activity
}
}
}
Trong ví dụ này, Runnable ẩn danh nắm giữ một tham chiếu ngầm đến Activity. Khi luồng còn sống — Activity không thể được GC thu gom, ngay cả khi người dùng đã đóng màn hình. Việc sửa chữa với WeakReference + lớp tĩnh phá vỡ chuỗi này và cho phép Activity được giải phóng.
Câu hỏi thường gặp
GC trong Android (ART) là trình thu gom thế hệ với tính năng nén đồng thời, được tối ưu cho thiết bị di động có bộ nhớ hạn chế. Java GC (G1, ZGC) là các trình thu gom phía máy chủ với heap lớn và thời gian tạm dừng có thể dự đoán. ART GC không sử dụng cờ JVM — tất cả việc điều chỉnh được thực hiện tự động ở cấp độ HĐH.
Stop-The-World là thời điểm trình thu gom tạm dừng tất cả các luồng ứng dụng để duyệt đồ thị đối tượng hoặc giải phóng bộ nhớ một cách an toàn. STW càng lâu, jank càng dễ nhận thấy. ART đã giảm thời gian STW điển hình xuống 2–4 ms nhờ kiến trúc thế hệ của nó.
Sử dụng Android Studio Memory Profiler — nó hiển thị sự tăng trưởng của heap, số lượng cấp phát và cho phép thực hiện Heap Dump. Để phân tích sâu, sử dụng LeakCanary — thư viện tự động phát hiện rò rỉ và hiển thị chuỗi tham chiếu ngăn cản việc thu gom GC.
Full GC là quá trình thu gom hoàn toàn tất cả các thế hệ heap, bao gồm cả Old Generation. Trong các ứng dụng di động, Full GC có thể kéo dài 50–200 ms, gây ra jank hoặc ANR đáng kể. Nguyên nhân chính: phân mảnh heap, rò rỉ bộ nhớ, vượt quá ngưỡng Old Generation.
Kotlin cung cấp coroutine với tính đồng thời có cấu trúc — việc hủy phạm vi tự động hủy tất cả các coroutine con, ngăn chặn rò rỉ. Kotlin cũng có delegate lazy cho khởi tạo trễ và các hàm phạm vi giúp giảm số lượng đối tượng tạm thời.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm