Rò rỉ bộ nhớ (Memory Leak) là tình huống ứng dụng giữ các tham chiếu đến những đối tượng không còn cần thiết, ngăn không cho bộ thu gom rác (GC) giải phóng bộ nhớ đã chiếm dụng. Theo LeakCanary, ngay trong các ứng dụng được viết tốt cũng có 3–5 rò rỉ trên mỗi 10.000 dòng mã. Mỗi rò rỉ dần dần làm giảm bộ nhớ khả dụng, dẫn đến chậm chạp và OutOfMemoryError.
Các điểm chính
Rò rỉ bộ nhớ (Memory Leak) là tình huống một đối tượng vẫn có thể truy cập được thông qua một chuỗi các tham chiếu mạnh (Strong Reference), mặc dù về mặt logic nó không còn cần thiết cho ứng dụng. Bộ thu gom rác (GC) coi đối tượng đó còn sống và không giải phóng bộ nhớ mà nó chiếm giữ. Kết quả là bộ nhớ heap khả dụng liên tục giảm và tần suất tạm dừng GC tăng lên.
Khác với các ngôn ngữ có quản lý bộ nhớ thủ công (C, C++), trong Java/Kotlin, rò rỉ không phải là free() bị quên mà là một tham chiếu bị quên. Chừng nào còn tồn tại một strong reference từ GC Root đến đối tượng bị rò rỉ, GC sẽ coi nó là cần thiết. GC Root điển hình: trường tĩnh, luồng hoạt động, ngăn xếp cuộc gọi, tham chiếu toàn cục JNI.
Sự nguy hiểm của rò rỉ là hiệu ứng tích lũy của chúng. Một rò rỉ 100 KB không đáng chú ý, nhưng 100 rò rỉ như vậy chiếm 10 MB và ứng dụng bắt đầu chậm do GC thường xuyên. Khối lượng rò rỉ tới hạn dẫn đến OutOfMemoryError và ứng dụng bị crash. Triệu chứng rò rỉ: tăng trưởng tiêu thụ bộ nhớ liên tục trên biểu đồ Profiler, tạm dừng GC thường xuyên với STW (Stop The World) và suy giảm hiệu suất UI.
Năm loại rò rỉ bao phủ 95% trường hợp trong phát triển di động. Mỗi loại có nguyên nhân riêng và mẫu mã đặc trưng.
Rò rỉ nổi tiếng nhất trong Android là lưu trữ tham chiếu tĩnh đến Activity hoặc Context. Mã điển hình: một trường Activity tĩnh không được đặt thành null trong onDestroy(). Khi trường tĩnh còn sống, toàn bộ Activity với cây View của nó (có thể chiếm 1–10 MB) vẫn tồn tại. Đây là rò rỉ kinh điển mà LeakCanary tìm thấy đầu tiên.
Giải pháp: không bao giờ lưu trữ Activity hoặc Context trong các trường tĩnh. Sử dụng Application Context cho các singleton tồn tại lâu hơn Activity. Nếu cần tham chiếu đến Activity, hãy sử dụng WeakReference<Activity>.
object MySingleton {
private var weakActivity: WeakReference<Activity>? = null
fun attach(activity: Activity) {
weakActivity = WeakReference(activity)
}
}
Các lớp ẩn danh và lớp nội không tĩnh ngầm giữ tham chiếu đến lớp chứa. Một Runnable được truyền cho Handler thực thi sau onDestroy() giữ toàn bộ Activity. Một callback Retrofit capture Activity cũng làm điều tương tự. Đây là loại rò rỉ nguy hiểm nhất — tham chiếu ngầm không hiển thị trong mã.
Biểu thức object và lambda trong Kotlin cũng capture tham chiếu đến lớp bên ngoài. Hãy làm cho các lớp nội trở nên tĩnh (hoặc top-level trong Kotlin) và truyền tham chiếu bên ngoài qua WeakReference. Đối với lambda, sử dụng cách tiếp cận Lifecycle-aware với viewLifecycleOwner.
Đăng ký dịch vụ hệ thống mà không hủy đăng ký là rò rỉ trực tiếp. SensorManager, LocationManager, NotificationListener được đăng ký trong onResume() mà không gọi unregister trong onPause() giữ Activity. Tương tự: RxJava Disposable không được thêm vào CompositeDisposable và coroutine được khởi chạy qua GlobalScope.
Sử dụng các thành phần Lifecycle-aware: observe() với LifecycleOwner tự động hủy đăng ký khi onDestroy(). Đối với RxJava — viewLifecycleOwner.lifecycle.addObserver với DisposableObserver. Đối với coroutine — lifecycleScope.launch() được gắn với vòng đời.
// hủy đăng ký tự động qua Lifecycle
viewModel.userData.observe(viewLifecycleOwner) { data ->
updateUI(data)
}
// coroutine với lifecycleScope
lifecycleScope.launch {
viewModel.loadData().collect { render(it) }
}
Bitmap chiếm một lượng đáng kể bộ nhớ heap: một bitmap FullHD là 1920 × 1080 × 4 byte = 8,3 MB. Nếu Bitmap được tạo cho mỗi mục trong danh sách và recycle() không được gọi khi ẩn, bộ nhớ sẽ nhanh chóng cạn kiệt. Trên các phiên bản Android cũ (trước 3.0), Bitmap được lưu trữ trong bộ nhớ native, nhưng trên các phiên bản hiện đại, nó nằm trong heap Dalvik/ART và GC chỉ có thể giải phóng nó nếu không có strong reference.
Sử dụng Glide hoặc Coil để tải hình ảnh — các thư viện này tự động quản lý bộ nhớ đệm và tái chế. Nếu làm việc trực tiếp với Bitmap, hãy gọi bitmap.recycle() cho các hình ảnh lớn không còn hiển thị và sử dụng inSampleSize để tải các bản sao giảm kích thước.
Fragment có hai vòng đời: của chính Fragment và của View. Sau onDestroyView(), cây View bị phá hủy, nhưng bản thân Fragment có thể vẫn tồn tại trong bộ nhớ nếu có tham chiếu bên ngoài. Một lỗi điển hình là lưu trữ tham chiếu đến Fragment trong bộ điều hợp ViewPager hoặc trong đồ thị điều hướng không được xóa khi hủy.
Không bao giờ lưu trữ tham chiếu đến Fragment trong các trường của đối tượng tồn tại lâu. Sử dụng childFragmentManager cho các fragment lồng nhau và observe() với LifecycleOwner để truyền dữ liệu giữa chúng. ViewPager2 đã giải quyết vấn đề này ở cấp API: FragmentTransactionAdapter quản lý vòng đời một cách chính xác.
Phát hiện rò rỉ yêu cầu xác minh hai sự thật: bộ nhớ không trở lại sau vòng đời dự kiến và số lượng đối tượng của một loại nhất định tăng lên mà không giảm. Quy trình chẩn đoán bao gồm ba giai đoạn.
Giai đoạn đầu tiên — kiểm tra trực quan qua Memory Profiler trong Android Studio. Mở tab Memory, thực hiện hành động mục tiêu (mở và đóng màn hình), nhấn GC (Garbage Collection) và xem bộ nhớ có trở về mức ban đầu không. Nếu sau 3–4 chu kỳ mở-đóng, bộ nhớ liên tục tăng — có rò rỉ.
Giai đoạn thứ hai — chụp Heap Dump. Trong Memory Profiler, nhấn Dump Java Heap. Mở tệp .hprof kết quả trong Android Studio: bạn sẽ thấy tất cả các đối tượng trong heap với kích thước và tham chiếu. Tìm các lớp có số lượng phải bằng 0 sau khi đóng màn hình. Ví dụ, MainActivity với số lượng 2 sau khi đóng là rò rỉ rõ ràng.
Giai đoạn thứ ba — phân tích Retained Size và GC Root. Trong Android Studio, phân tích Retained Size: bao nhiêu bộ nhớ sẽ được giải phóng nếu bạn xóa đối tượng này. Đường dẫn từ GC Root đến đối tượng cho thấy điều gì giữ nó: Static field → HashMap → Activity — và bạn thấy điểm rò rỉ. Bảng điều khiển Reference hiển thị tất cả những người giữ đối tượng.
Bốn công cụ bao phủ việc tìm rò rỉ từ phát hiện tự động đến phân tích Heap Dump chuyên sâu.
| Công cụ | Phương pháp | Định dạng kết quả |
|---|---|---|
| LeakCanary | Giám sát tự động | Heap Dump + stack trace rò rỉ |
| Android Memory Profiler | Giám sát thủ công | Biểu đồ bộ nhớ + Heap Dump |
| MAT (Eclipse) | Phân tích chuyên sâu | Báo cáo Dominator Tree + đường dẫn GC Root |
| Perfetto | Theo dõi toàn hệ thống | Dòng thời gian + bộ nhớ native |
LeakCanary là công cụ không thể thiếu cho bất kỳ dự án Android nào. Nó tự động phát hiện rò rỉ khi kết thúc vòng đời Activity/Fragment và hiển thị vị trí rò rỉ chính xác với stack trace. Tích hợp: một dòng trong build.gradle. LeakCanary 2.x không yêu cầu khởi tạo thủ công — nó tự động đăng ký Application Watcher.
Phòng ngừa rò rỉ được tích hợp vào quy trình phát triển thông qua một bộ quy tắc và công cụ kiểm tra mã ở mỗi giai đoạn.
Không bao giờ lưu trữ tham chiếu đến Activity, Fragment hoặc View trong trường tĩnh, singleton hoặc đối tượng tồn tại lâu. Nếu không thể tránh tham chiếu, hãy sử dụng WeakReference hoặc lưu trữ dữ liệu qua ViewModel, tồn tại chính xác trong thời gian cần thiết và không giữ View trực tiếp.
ViewModel và LiveData từ Android Architecture Components giải quyết vấn đề vòng đời ở cấp độ kiến trúc. ViewModel sống sót qua xoay màn hình và không chứa tham chiếu đến View. LiveData tự động hủy đăng ký observer khi onDestroy(). Sử dụng chúng thay vì đăng ký thủ công các dịch vụ hệ thống.
Khi review mã, hãy chú ý đến: trường tĩnh với kiểu Context/View, lớp ẩn danh, lambda capture Activity, đăng ký thủ công, RxJava disposable không có composite, lưu trữ Fragment qua Bundle. Trong Kotlin, kiểm tra thêm coroutine với launch không có ràng buộc vòng đời.
LeakCanary có thể hoạt động như một phần của pipeline kiểm thử: chạy kiểm thử chấp nhận với LeakCanary và đánh dấu build thất bại nếu tìm thấy rò rỉ. Điều này ngăn rò rỉ đến môi trường sản xuất. Bổ sung kiểm tra với quy tắc StaticFieldLeak của Android Lint — nó tìm các rò rỉ tiềm ẩn ở cấp phân tích tĩnh.
// LeakCanary trong kiểm thử
class LeakTest {
@Test
fun activityShouldNotLeak() {
ActivityScenario.launch(MainActivity::class.java)
.close()
LeakAssertions.assertNoLeak() // thất bại nếu có rò rỉ
}
}
Câu hỏi thường gặp
Rò rỉ là nguyên nhân, và OutOfMemoryError là kết quả. Một rò rỉ không dẫn đến OOM, nhưng sự tích tụ của hàng chục rò rỉ làm cạn kiệt Heap. OOM là một ngoại lệ nghiêm trọng, trong khi rò rỉ là một mẫu dẫn đến nó theo thời gian.
Thông qua Android Memory Profiler: mở và đóng màn hình 5 lần, sau mỗi lần đóng hãy gọi GC. Nếu bộ nhớ không trở về mức cơ bản — có rò rỉ. Chụp Heap Dump và tìm trong danh sách lớp Activity có số lượng lớn hơn 0 sau khi đóng.
Một phần. Kotlin giải quyết vấn đề null-safety nhưng không quản lý strong references. Coroutine với lifecycleScope và viewModelScope ngăn rò rỉ từ các tác vụ nền, trong khi sealed class và data class giảm số trạng thái dẫn đến rò rỉ. Sự bảo vệ chính là các mẫu kiến trúc, không phải tính năng ngôn ngữ.
LeakCanary đôi khi cho kết quả dương tính giả: một đối tượng có thể bị hệ thống giữ tạm thời (ví dụ: InputMethodManager giữ View cuối cùng). Hãy kiểm tra thủ công: nếu Retained Size < 1 KB và GC Root là một dịch vụ hệ thống, thì đó có thể là dương tính giả.
Không. Rò rỉ có thể xảy ra trên bất kỳ nền tảng nào có GC: iOS (Swift/Objective-C), Flutter (Dart), trình duyệt web (JavaScript). Cơ chế đều giống nhau — strong reference từ GC Root. Trên iOS, ARC tự động quản lý bộ nhớ, nhưng retain cycle giữa các đối tượng tạo ra cùng một rò rỉ.
Tóm tắ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