Rò rỉ bộ nhớ: nó là gì, các kịch bản điển hình và chẩn đoán

Tác giả: IT Sectr Đã đăng: 2026-07-29 Thời gian đọc: 10 phút

Rò rỉ bộ nhớ (memory leak) — tình huống ứng dụng không giải phóng bộ nhớ bị chiếm dụng bởi các đối tượng không còn cần thiết. Trong phát triển di động, điều này đặc biệt quan trọng: heap hạn chế và không có swap dẫn đến OutOfMemoryError và ứng dụng bị treo. Theo Purdue University (2022), 35% ứng dụng Android trên Google Play chứa ít nhất một rò rỉ bộ nhớ. Hãy xem xét các kịch bản điển hình, công cụ chẩn đoán và phương pháp khắc phục.

Những điểm chính

  • GC Root — điểm vào mà qua đó trình dọn rác xác định các đối tượng còn sống
  • Rò rỉ Context — truyền Activity Context vào singleton giữ toàn bộ hệ phân cấp View
  • Handler với postDelayed — nếu Activity bị hủy, Handler ngăn nó được GC dọn
  • Heap dump — phương pháp phân tích rò rỉ chính qua MAT hoặc Android Profiler
  • SoftReference — thay thế cho WeakReference cho bộ đệm với tự động xóa khi thiếu bộ nhớ

Rò rỉ bộ nhớ trong ứng dụng di động là gì?

Rò rỉ bộ nhớ — tình huống khi bộ nhớ được cấp phát không được trả lại cho hệ thống sau khi đối tượng không còn cần thiết cho chương trình. Trình dọn rác coi đối tượng đó là còn sống vì có một chuỗi tham chiếu hoạt động từ GC Root trỏ đến nó.

Trong Java/Kotlin, trình dọn rác hoạt động tự động, nhưng nó không thể xác định rằng một đối tượng không cần thiết về mặt logic nếu có tham chiếu kỹ thuật đến nó. Nhà phát triển phải chủ động ngắt các kết nối không cần thiết. Trong Swift/Objective-C, ARC tự động đếm tham chiếu, nhưng retain cycles ngăn bộ đếm về không.

Nguy hiểm chính của rò rỉ là hiệu ứng tích lũy. Mỗi rò rỉ tiêu thụ một lượng nhỏ bộ nhớ, nhưng với các chuyển đổi màn hình lặp lại (xoay màn hình, mở/đóng Activity), rò rỉ tích tụ cho đến khi cạn kiệt giới hạn heap.

Rò rỉ khác với phình to thế nào?

Rò rỉ — đối tượng không thể truy cập được từ mã nhưng không bị GC xóa. Phình to — đối tượng cần thiết về mặt logic nhưng được lưu trữ với số lượng quá mức. Ví dụ về phình to: bộ đệm ảnh 100 MB với tập làm việc 30 MB. Cả hai vấn đề đều dẫn đến OOM, nhưng nguyên nhân và phương pháp xử lý khác nhau.

Trình dọn rác hoạt động thế nào và tại sao rò rỉ xảy ra?

ART (Android Runtime) sử dụng dọn rác theo thế hệ với nén đồng thời. Bộ nhớ được chia thành thế hệ trẻ (Young), thế hệ già (Old) và đối tượng lớn (Large). Các đối tượng sống qua nhiều chu kỳ GC được chuyển đến Old generation, nơi việc dọn ít xảy ra hơn — điều này tăng tốc các chu kỳ thông thường.

GC bắt đầu khi heap đạt đến một ngưỡng chiếm dụng nhất định (thường là 75-85%). Trong thời gian GC, tất cả các luồng của ứng dụng đều bị tạm dừng (STW — Stop The World). Càng nhiều đối tượng sống, thời gian tạm dừng càng lâu. Rò rỉ làm tăng số lượng đối tượng sống, kéo dài thời gian tạm dừng GC.

Trình dọn xác định các đối tượng sống bằng cách duyệt đồ thị từ GC Roots: trường tĩnh, biến stack của các luồng đang hoạt động, tham chiếu JNI. Bất kỳ đối tượng nào có thể truy cập qua tham chiếu từ các gốc này đều được coi là sống — ngay cả khi nhà phát triển biết rằng nó không còn cần thiết.

kotlin
// Ví dụ: bộ sưu tập tĩnh như GC Root — rò rỉ vĩnh viễn
object GlobalHolder {
    val listeners = mutableListOf<WeakReference<Any>>()
}

class LeakingFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle ?= null) {
        super.onCreate(savedInstanceState)
        GlobalHolder.listeners.add(WeakReference(this))
        // WeakReference không ngăn GC — hành vi đúng
    }
}

WeakReference giải quyết vấn đề: GC bỏ qua tham chiếu yếu khi xác định đối tượng sống. Nếu chỉ còn tham chiếu yếu đến một đối tượng, nó sẽ được dọn trong chu kỳ GC gần nhất.

Các kịch bản rò rỉ điển hình trong Android và iOS

Activity Context — kịch bản rò rỉ phổ biến nhất trong Android. Nếu singleton, trường tĩnh hoặc dịch vụ tồn tại lâu lưu trữ tham chiếu đến Activity Context, toàn bộ Activity với tất cả Views không thể được GC dọn. Giải pháp: sử dụng Application Context cho các đối tượng tồn tại lâu.

Handler và tin nhắn đã gửi — Handler.postDelayed(runnable, delay) đặt một tin nhắn vào hàng đợi Main Looper. Nếu Activity bị hủy trước khi hết thời gian trễ, tin nhắn vẫn ở trong hàng đợi và giữ tham chiếu qua Runnable → lớp ẩn danh → lớp bên ngoài (Activity).

kotlin
class SafeActivity : AppCompatActivity() {
    private val mainHandler = Handler(Looper.getMainLooper())
    private val callback = Runnable { /* update UI */ }

    override fun onResume() {
        super.onResume()
        mainHandler.postDelayed(callback, 5000)
    }

    override fun onPause() {
        mainHandler.removeCallbacks(callback) // bắt buộc: xóa hàng đợi
        super.onPause()
    }
}

Lớp bên trong — lớp bên trong không tĩnh có tham chiếu ngầm đến thể hiện của lớp bên ngoài. Nếu lớp bên ngoài là Activity và lớp bên trong được truyền ra ngoài (ví dụ, vào RecyclerView.Adapter), Activity không thể được dọn.

  • TimerTask và ScheduledExecutorService — tác vụ đã lên lịch trước khi Activity bị hủy
  • BroadcastReceiver — không hủy đăng ký trong onPause/onDestroy tiếp tục giữ Context
  • ViewModel với tham chiếu đến View — ViewModel sống lâu hơn Activity, tham chiếu đến View dẫn đến rò rỉ
  • Retrofit Call — nếu Call không bị hủy, phản hồi đến Fragment đã bị hủy

Công cụ chẩn đoán rò rỉ bộ nhớ

Android Studio Memory Profiler — công cụ tích hợp để giám sát heap thời gian thực. Hiển thị biểu đồ bộ nhớ đã dùng, số lượng cấp phát và đối tượng theo loại. Cho phép ghi heap dump và xuất sang định dạng HPROF để phân tích trong MAT.

Eclipse MAT (Memory Analyzer Tool) — trình phân tích heap dump trên máy tính để bàn. Tự động xây dựng báo cáo Leak Suspects, làm nổi bật các đối tượng có retained size lớn nhất và đề xuất chuỗi GC Root nghi ngờ cho mỗi đối tượng khả nghi.

Xcode Memory Graph Debugger — dành cho iOS. Tạm dừng ứng dụng và trực quan hóa đồ thị đối tượng. Retain cycles được tô màu đỏ; bạn có thể nhấp vào bất kỳ đối tượng nào để xem retain count và tham chiếu của nó.

Công cụKhả năngĐộ phức tạp
Memory ProfilerBiểu đồ thời gian thực, heap dump, Object Allocation TrackingThấp
Eclipse MATDominator tree, Leak Suspects, truy vấn OQLTrung bình
LeakCanaryPhát hiện tự động, dấu vết rò rỉ trong thông báoTối thiểu
Xcode Memory GraphĐồ thị trực quan retain cycles, danh sách đối tượng sốngThấp

Theo Uber Engineering Blog, việc tích hợp lược sử bộ nhớ tự động (LeakCanary + phân tích heap dump) vào đường ống CI/CD giảm 60% sự cố liên quan đến bộ nhớ trong sản xuất trong vòng 3 tháng.

Phương pháp loại bỏ rò rỉ

Thay thế Context — nếu đối tượng sống lâu hơn Activity, hãy sử dụng applicationContext. Tất cả các đối tượng tồn tại lâu (singleton, repository, helper cơ sở dữ liệu) nên nhận Application Context, không phải Activity Context. Ngoại lệ: thành phần UI cần truy cập theme hoặc tài nguyên cụ thể của Activity.

Thành phần nhận biết vòng đời — sử dụng LifecycleObserver, DefaultLifecycleObserver hoặc phần mở rộng phản ứng tự động hủy đăng ký tại onDestroy. Android Jetpack cung cấp lifecycleScope và viewModelScope, được dọn dẹp bởi sự kiện vòng đời tương ứng.

Lớp bên trong tĩnh — nếu lớp bên trong không cần truy cập vào các trường của lớp bên ngoài, hãy làm cho nó static. Lớp bên trong tĩnh không có tham chiếu ngầm đến lớp bên ngoài. Nếu cần truy cập, hãy sử dụng WeakReference cho tham chiếu tường minh.

kotlin
class MyActivity : AppCompatActivity() {

    // ❌ Lớp bên trong không tĩnh — tham chiếu ngầm đến MyActivity
    inner class BadListener : SomeListener {
        override fun onEvent() { /*...*/ }
    }

    // ✅ Lớp bên trong tĩnh — không có tham chiếu ngầm
    class GoodListener(private val activityRef: WeakReference<MyActivity>) : SomeListener {
        override fun onEvent() { /*...*/ }
    }
}

Trong iOS, sử dụng danh sách chụp: [weak self] trong các closure có thể sống lâu hơn người tạo ra chúng. Cho delegate, sử dụng tham chiếu yếu (weak var delegate). Cho các closure được đảm bảo chỉ được gọi trong vòng đời của self, có thể sử dụng [unowned self], nhưng cẩn thận — truy cập đối tượng đã giải phóng sẽ gây treo.

Câu hỏi thường gặp

Làm thế nào để tìm rò rỉ mà không cần công cụ đặc biệt?

Trong Android, thực hiện một số chuyển đổi giữa các màn hình (Activity A → B → A → B) và kiểm tra adb shell dumpsys meminfo package_name. Nếu Total PSS tăng ổn định và không trở về giá trị ban đầu — có rò rỉ. Trong iOS, tương tự: sử dụng Debug Memory Graph trong Xcode để kiểm tra trực quan.

Kotlin coroutine có thể gây rò rỉ không?

Có, nếu CoroutineScope không bị hủy khi thành phần bị phá hủy. Coroutine được khởi chạy trong GlobalScope tiếp tục thực thi ngay cả sau finish() của Activity. Giải pháp: sử dụng viewModelScope (bị hủy trong onCleared) hoặc lifecycleScope (bị hủy trong onDestroy). Cho phạm vi tùy chỉnh, hãy tạo phạm vi nhận biết vòng đời qua LifecycleOwner.

Bitmap ảnh hưởng đến rò rỉ thế nào?

Bitmap lưu trữ dữ liệu pixel trong heap gốc (native heap), không phải heap Java. Điều này có nghĩa là Java GC không thấy kích thước thực của Bitmap. Nếu Bitmap không được tái chế bằng recycle() hoặc tham chiếu không được đặt thành null, bộ nhớ gốc sẽ không được giải phóng. Sử dụng BitmapFactory với inSampleSize để tải các bản sao thu nhỏ và Glide/Coil để quản lý bộ đệm tự động.

Rò rỉ qua trường tĩnh là gì?

Trường tĩnh — là một GC Root. Nó tồn tại miễn là lớp được tải (trong Android — miễn là Process còn sống). Nếu trường tĩnh tham chiếu đến Activity, Bitmap, View hoặc bất kỳ đối tượng nặng nào khác, đối tượng đó sẽ không bao giờ được GC dọn. Trường tĩnh là một tham chiếu vĩnh viễn. Giải pháp: chỉ lưu trữ WeakReference hoặc đặt trường tĩnh thành null trong onDestroy.

Làm thế nào để tránh rò rỉ trong iOS với ARC?

ARC tự động giải phóng đối tượng khi bộ đếm tham chiếu mạnh giảm về không. Retain cycle là cách duy nhất để rò rỉ trong ARC. Luôn sử dụng weak cho tham chiếu cha→con nơi con có thể sống lâu hơn cha (delegate, data source). Cho closure, sử dụng danh sách chụp [weak self] và kiểm tra self có nil không bên trong closure.

Tổng kết

  • Rò rỉ bộ nhớ — đối tượng không thể truy cập từ mã nhưng không bị GC xóa vì có tham chiếu hoạt động từ GC Root
  • GC Roots bao gồm trường tĩnh, biến stack và tham chiếu JNI; bất kỳ đối tượng nào truy cập được từ chúng đều sống
  • Rò rỉ Context — vấn đề phổ biến nhất trong Android: truyền Activity Context vào singleton hoặc trường tĩnh
  • Handler và lớp bên trong — nguyên nhân phổ biến thứ hai: tin nhắn chưa hủy trong hàng đợi Looper giữ tham chiếu đến Activity
  • LeakCanary — công cụ phát hiện tự động tiêu chuẩn; lấy heap dump và hiển thị chuỗi GC Root chính xác
  • lifecycleScope và viewModelScope giải quyết vấn đề rò rỉ qua coroutine — tự động hủy khi phá hủy
  • Lược sử bộ nhớ trong CI/CD: LeakCanary ở chế độ gỡ lỗi + phân tích heap dump trong lần chạy thử nghiệm nên chặn việc hợp nhất khi có rò rỉ mới

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.

Thảo luận dự án

Đọc thêm