OutOfMemoryError là một ngoại lệ nghiêm trọng xảy ra khi Máy ảo Java (JVM) hoặc Android Runtime (ART) không thể cấp phát bộ nhớ cho một đối tượng mới do không đủ không gian trong Heap. Theo Square Engineering, 70% OutOfMemoryError trong ứng dụng di động là do rò rỉ bộ nhớ gây ra, không phải do vượt quá giới hạn thực tế. Hiểu nguyên nhân của OOM là chìa khóa cho sự ổn định của ứng dụng.
Những Điểm Chính
OutOfMemoryError (OOM) là một ngoại lệ thuộc họ VirtualMachineError trong Java/Kotlin báo hiệu không thể cấp phát bộ nhớ cho một đối tượng mới. Không giống như các ngoại lệ đã kiểm tra, OOM là một Error và không yêu cầu xử lý thông qua catch — mặc dù về mặt kỹ thuật có thể bắt được nó. Sau khi OOM xảy ra, ứng dụng thường ở trạng thái không ổn định và nên kết thúc nó.
Trên Android, mỗi ứng dụng có một giới hạn Heap do nhà sản xuất thiết bị đặt ra. Đối với điện thoại thông minh hiện đại có 6+ GB RAM, giới hạn là 256–512 MB, đối với thiết bị giá rẻ — 128–192 MB. Khi tổng khối lượng của tất cả các đối tượng sống vượt quá giới hạn này, ART ném ra OutOfMemoryError.
Điều quan trọng là hiểu: OOM không phải lúc nào cũng có nghĩa là thiết bị đã hết bộ nhớ vật lý. Nó có nghĩa là ứng dụng đã cạn kiệt giới hạn Heap do hệ thống đặt ra. Các ứng dụng khác có thể có bộ nhớ trống, nhưng ứng dụng của bạn không thể sử dụng nó do sự cô lập tiến trình trong Android.
Năm kịch bản thường xuyên dẫn đến OOM trong ứng dụng di động. Mỗi kịch bản liên quan đến một loại dữ liệu hoặc thao tác cụ thể.
Bitmap là người tiêu thụ bộ nhớ chính trong ứng dụng Android. Tải ảnh FullHD (1920 × 1080) ở kích thước gốc chiếm 8,3 MB ở định dạng ARGB_8888. Nếu có 50 ảnh như vậy trong RecyclerView — đó là 415 MB, vượt quá Heap của bất kỳ thiết bị nào. Tải ảnh mà không có inSampleSize đảm bảo OOM trên các thiết bị yếu.
Sử dụng Glide hoặc Coil để tự động điều chỉnh tỷ lệ. Các thư viện này tải ảnh với kích thước phù hợp với View, không phải độ phân giải gốc. Để sử dụng trực tiếp BitmapFactory.Options, hãy áp dụng inSampleSize: tính nó như một lũy thừa của 2 để kích thước cuối cùng không vượt quá 2048 × 2048 pixel. Ngoài ra, sử dụng RGB_565 thay vì ARGB_8888 cho ảnh không có độ trong suốt — điều này giảm một nửa mức tiêu thụ bộ nhớ.
fun loadScaledBitmap(path: String, reqWidth: Int): Bitmap? {
val opts = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, opts)
opts.inSampleSize = calculateSampleSize(opts.outWidth, reqWidth)
opts.inJustDecodeBounds = false
return BitmapFactory.decodeFile(path, opts)
}
Một rò rỉ vài KB sẽ không gây ra OOM. Nhưng hàng chục rò rỉ trên mỗi màn hình tích lũy lại: mỗi lần chuyển màn hình thêm một rò rỉ, GC không thể giải phóng đối tượng và Heap đầy lên. Một mẫu điển hình: người dùng mở và đóng màn hình hồ sơ 20 lần → Heap tăng 200 MB → ứng dụng sập với OOM.
Cài đặt LeakCanary vào dự án để phát hiện rò rỉ tự động. Nó sẽ hiển thị từng đối tượng bị rò rỉ với dấu vết ngăn xếp chính xác. Sau khi sửa tất cả rò rỉ, mức tiêu thụ Heap trở nên ổn định: sau khi đóng màn hình, bộ nhớ trở về mức cơ bản.
Tải toàn bộ tệp vào byte[] là con đường trực tiếp đến OOM. Tệp JSON 50 MB khi phân tích cú pháp sẽ tạo một chuỗi có cùng kích thước cộng với mô hình DOM. Tệp video được tải vào bộ nhớ, bộ đệm âm thanh và tập dữ liệu protobuf lớn — tất cả đều có thể vượt quá giới hạn Heap trong một thao tác duy nhất.
Xử lý dữ liệu lớn bằng luồng: InputStream với bộ đệm 4–8 KB, trình phân tích JSON luồng (Jackson hoặc Gson với JsonReader), MediaCodec cho video. Không bao giờ gọi File.readBytes() trên các tệp lớn hơn 10% Heap khả dụng.
Việc tạo đối tượng chuyên sâu trong vòng lặp mà không có GC trung gian có thể dẫn đến OOM, đặc biệt trên các thiết bị có Heap nhỏ. Ví dụ: tạo 100.000 đối tượng trong vòng lặp for không vừa trong Heap trước khi GC có thể thu thập chúng. Điều này phổ biến hơn trong trò chơi và trình chỉnh sửa đồ họa.
Sử dụng Object Pool cho các đối tượng được tạo và hủy hàng loạt. Đối với dữ liệu số, sử dụng kiểu nguyên thủy (FloatArray thay vì List<Float>). RecyclerView với ViewHolder Pool giải quyết vấn đề này cho các thành phần UI.
Phân mảnh là trạng thái mà tổng thể có đủ bộ nhớ trống, nhưng không có khối liên tục cho một đối tượng mới. ART nén Heap trong quá trình GC, nhưng không phải lúc nào cũng thành công. Các mảng lớn (Bitmap, byte[]) nhạy cảm nhất với phân mảnh.
ART trên Android 8+ sử dụng GC Thế hệ, giúp giảm phân mảnh bằng cách phân tách các đối tượng trẻ và già. Tuy nhiên, tránh cấp phát các mảnh có kích thước khác nhau trong cùng một pool — hãy cố gắng sử dụng bộ đệm được cấp phát trước có kích thước cố định.
Giới hạn Heap trong Android không phải là hằng số — nó phụ thuộc vào nhà sản xuất, mẫu thiết bị và phiên bản HĐH. Google đặt ra các yêu cầu tối thiểu thông qua Tài liệu Định nghĩa Tương thích (CDD), nhưng các nhà sản xuất đặt ra các giá trị thực tế.
| Danh mục Thiết bị | Heap Điển hình | largeHeap |
|---|---|---|
| Giá rẻ (1–2 GB RAM) | 128–192 MB | 256–384 MB |
| Tầm trung (3–4 GB RAM) | 256–384 MB | 512 MB |
| Cao cấp (6+ GB RAM) | 384–512 MB | 768 MB–1 GB |
| Máy tính bảng (4+ GB RAM) | 256–512 MB | 768 MB |
| Wear OS | 32–64 MB | Không có |
Bạn có thể yêu cầu giới hạn tăng lên thông qua android:largeHeap="true" trong tệp kê khai. Sử dụng nó một cách thận trọng: tăng Heap không giải quyết được vấn đề rò rỉ và có thể làm xấu trải nghiệm người dùng nếu hệ thống buộc phải đóng các ứng dụng khác để giải phóng bộ nhớ cho ứng dụng của bạn. Đối với Wear OS, giới hạn Heap là tối thiểu — chỉ 32–64 MB, largeHeap không khả dụng ở đây và tiết kiệm bộ nhớ quan trọng gấp đôi.
Chẩn đoán OOM đòi hỏi phân tích Heap Dump và hiểu đối tượng nào đang tiêu thụ bộ nhớ. Android Studio cung cấp tất cả các công cụ cần thiết.
Bước 1: Chụp khoảnh khắc OOM. Trong Android Memory Profiler, nhấp Record memory allocations và thực hiện kịch bản gây ra sự cố. Profiler sẽ hiển thị sự gia tăng cấp phát trước OOM. Nếu OOM không thể tái tạo, hãy giảm Heap qua android:smallHeap trong bản gỡ lỗi hoặc sử dụng DDMS với lệnh gọi GC thủ công.
Bước 2: Chụp Heap Dump tại thời điểm tải cao nhất (trước OOM). Mở Dump trong Android Studio: tab Classes được sắp xếp theo Retained Size. Các đối tượng lớn nhất là Bitmap, byte[], String. Đối với mỗi Bitmap, kiểm tra kích thước (chiều rộng × chiều cao × 4 byte) và đường dẫn tải qua Stack Trace.
Bước 3: Phân tích số lượng đối tượng trùng lặp. Nếu bạn thấy 200 Fragment hoặc Activity giống hệt nhau — đó là rò rỉ. Nếu 500 Bitmap có cùng kích thước — đó là vấn đề bộ nhớ đệm ảnh. MAT (Memory Analyzer Tool) cung cấp phân tích sâu hơn với Dominator Tree hiển thị đối tượng nào giữ 80% Heap.
// Lệnh Heap Dump qua adb
adb shell am dumpheap com.example.app /data/local/tmp/dump.hprof
adb pull /data/local/tmp/dump.hprof.
Một chiến lược toàn diện phòng ngừa OOM bao gồm năm cấp độ bảo vệ: từ quyết định kiến trúc đến giám sát sản xuất.
ViewModel + Repository tách dữ liệu khỏi UI và ngăn giữ lại View khi xoay màn hình. ViewModel tồn tại lâu hơn Activity, dữ liệu của nó không bị mất và View có thể được tạo lại mà không trùng lặp dữ liệu trong bộ nhớ. Sử dụng StateFlow thay vì LiveData để quản lý trạng thái rõ ràng.
Glide là thư viện bắt buộc để làm việc với hình ảnh. Nó tự động điều chỉnh tỷ lệ, lưu vào bộ nhớ đệm (đĩa + bộ nhớ) và tái chế Bitmap. Cấu hình diskCacheStrategy và skipMemoryCache cho danh sách lớn. Đối với hình ảnh động, sử dụng Glide với GIF/WebP — chúng chiếm ít bộ nhớ hơn một chuỗi Bitmap.
Firebase Performance Monitoring theo dõi mức tiêu thụ bộ nhớ theo thời gian thực. Đặt cảnh báo khi sử dụng Heap vượt quá 80% giới hạn — đó là tín hiệu để điều tra. Crashlytics thu thập OOM như một ngoại lệ và hiển thị trạng thái Heap cuối cùng được biết trước khi sập. Đối với Android 11+, sử dụng ApplicationExitInfo để phát hiện kết thúc OOM.
Hãy đảm bảo kiểm thử ứng dụng trên các thiết bị có Heap tối thiểu (128–192 MB). Trình giả lập với màn hình nhỏ và Heap nhỏ mô phỏng thiết bị giá rẻ. Nếu ứng dụng hoạt động trên thiết bị như vậy, sẽ không có vấn đề OOM trên các thiết bị cao cấp. Sử dụng Firebase Test Lab với các thiết bị thực từ các phân khúc giá khác nhau.
// Kiểm tra Heap khả dụng trước thao tác nặng
fun canAllocate(requiredBytes: Long): Boolean {
val runtime = Runtime.getRuntime()
val free = runtime.freeMemory()
return free > requiredBytes * 2 // bộ đệm 50%
}
Câu hỏi Thường gặp
Về mặt kỹ thuật là có, nhưng điều này không được khuyến nghị. Sau OOM, ứng dụng ở trạng thái không ổn định: các cấp phát mới có thể thất bại và một số đối tượng có thể được tạo một phần. Hành động hợp lý duy nhất trong catch là ghi nhật ký và khởi động lại Activity.
Giới hạn Heap khác nhau giữa các thiết bị. Một thao tác yêu cầu 300 MB sẽ sập trên thiết bị có giới hạn 192 MB nhưng sẽ thành công trên thiết bị cao cấp có 512 MB. Kiểm thử trên các thiết bị có thông số kỹ thuật tối thiểu để phát hiện các kịch bản OOM.
largeHeap tăng giới hạn nhưng không tăng tốc ứng dụng. Tạm dừng GC trở nên lâu hơn vì thu thập Heap lớn mất nhiều thời gian hơn. Hệ thống có thể đóng các ứng dụng nền để cung cấp bộ nhớ. Chỉ sử dụng largeHeap cho các ứng dụng khách quan cần nhiều bộ nhớ (máy ảnh, trình chỉnh sửa).
OOM là một ngoại lệ bên trong ứng dụng khi Heap không đủ. Việc hệ thống giết (Low Memory Killer) là quyết định của nhân Linux để giết một tiến trình nhằm giải phóng bộ nhớ cho các ứng dụng khác. Khi hệ thống giết, ứng dụng không nhận được ngoại lệ — tiến trình chỉ đơn giản kết thúc.
Công thức: chiều rộng × chiều cao × bytesPerPixel. ARGB_8888 = 4 B/pixel, RGB_565 = 2 B/pixel. Bitmap FullHD (1920 × 1080) ở ARGB_8888 = 8,3 MB. Bitmap 4K (3840 × 2160) = 33 MB. Luôn điều chỉnh tỷ lệ ảnh theo kích thước cần thiết để hiển thị trên màn hình.
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