OutOfMemoryError trong phát triển ứng dụng: nó là gì, nguyên nhân và phương pháp phòng ngừa

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

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 — một ngoại lệ khi không đủ Heap để tạo đối tượng mới
  • Heap — vùng bộ nhớ nơi tất cả các đối tượng Java/Kotlin tồn tại
  • Bitmap — người tiêu thụ Heap chính trong Android, nguồn OOM điển hình
  • Heap Dump — một ảnh chụp nhanh Heap để phân tích ai đang chiếm bao nhiêu bộ nhớ
  • Điều trị OOM đòi hỏi sửa rò rỉ và tối ưu hóa tiêu thụ bộ nhớ

OutOfMemoryError là gì

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.

Nguyên nhân chính của OutOfMemoryError

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 Không Có Tỷ Lệ

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ớ.

kotlin
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)
}

Rò rỉ Bộ nhớ (Tích lũy)

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ệp Lớn Trong Bộ nhớ

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.

Tạo Nhiều Đối tượng Trong Vòng Lặp

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 Heap

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

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ìnhlargeHeap
Giá rẻ (1–2 GB RAM)128–192 MB256–384 MB
Tầm trung (3–4 GB RAM)256–384 MB512 MB
Cao cấp (6+ GB RAM)384–512 MB768 MB–1 GB
Máy tính bảng (4+ GB RAM)256–512 MB768 MB
Wear OS32–64 MBKhô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 OutOfMemoryError

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.

text
// 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.

Chiến lược phòng ngừa OOM

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.

Quyết định Kiến trúc

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.

Quản lý Bitmap và Hình ảnh

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.

Giám sát Sản xuất

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.

Kiểm thử trên Thiết bị Yếu

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.

kotlin
// 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

Có thể bắt OutOfMemoryError bằng try-catch không?

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.

Tại sao OOM không xảy ra trên tất cả các thiết bị?

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 ảnh hưởng đến hiệu suất như thế nào?

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 khác với việc hệ thống giết tiến trình như thế nào?

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.

Bitmap thực sự tiêu thụ bao nhiêu bộ nhớ?

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

  • OutOfMemoryError — một ngoại lệ nghiêm trọng khi giới hạn Heap của ứng dụng bị cạn kiệt
  • Bitmap không có tỷ lệ — thủ phạm chính của OOM trong ứng dụng di động
  • Rò rỉ bộ nhớ gây ra 70% OOM thông qua tích lũy đối tượng sau mỗi lần chuyển
  • Giới hạn Heap dao động từ 128 MB trên thiết bị giá rẻ đến 512 MB trên thiết bị cao cấp
  • Heap Dump với phân tích Retained Size — công cụ chính để chẩn đoán OOM
  • Glide hoặc Coil là bắt buộc để làm việc với hình ảnh ở mọi kích thước
  • Kiểm thử trên thiết bị có Heap tối thiểu là điều bắt buộc cho mọi dự án

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