Overview quản lý bộ nhớ trong Android

Overview of memory management

Android Runtime (ART)Dalvik sử dụng phân trang (paging) và ánh xạ bộ nhớ (memory-mapping hay mmapping) để quản lý bộ nhớ. Điều này có nghĩa là bất kỳ bộ nhớ nào mà một ứng dụng thay đổi, cho dù bởi việc cấp phát các đối tượng mới hoặc thay đổi các trang bộ nhớ bị ánh xạ, thì vẫn tồn tại trong RAM và không thể được loại bỏ. Cách duy nhất để giải phóng bộ nhớ từ một ứng dụng là giải phóng các đối tượng tham chiếu mà ứng dụng lưu giữ, làm cho bộ nhớ sẵn sàng cho trình thu gom rác (garbage collector). Có một ngoại lệ: bất kỳ file nào được ghi vào mà không sửa đổi, chẳng hạn như mã nguồn, có thể được loại bỏ khỏi RAM nếu hệ thống muốn sử dụng bộ nhớ đó ở nơi khác.

Garbage collection

Về garbage collection, bạn có thể đọc thêm tại đây hoặc đây

Share memory

Để phù hợp với mọi thứ cần thiết trong RAM, Android cố gắng chia sẻ các trang RAM qua các process.

Mỗi process ứng dụng được fork từ một process gọi là Zygote. Zygote process start khi hệ thống khởi động, và tải các tài nguyên, mã nguồn framework. Để bắt đầu một process ứng dụng mới, hệ thống sẽ fork Zygote process, sau đó tải và chạy mã nguồn của ứng dụng trong process mới. Cách tiếp cận này cho phép hầu hết các trang RAM được cấp phát cho mã nguồn framework và tài nguyên được chia sẻ trên tất cả các process ứng dụng.

Hầu hết các dữ liệu tĩnh được ánh xạ bộ nhớ (mmapped) vào một process. Kỹ thuật này cho phép dữ liệu được chia sẻ giữa các process và cũng cho phép dữ liệu được xóa đi khi cần. Ví dụ về dữ liệu tĩnh bao gồm: Mã nguồn Dalvik, tài nguyên ứng dụng (app resources) và các thành phần project như mã nguồn trong các file .so.

Ở nhiều nơi, Android chia sẻ cùng một RAM động (dynamic RAM) trên các process bằng cách sử dụng các vùng bộ nhớ dùng chung được cấp phát tường minh (với ashmem hoặc gralloc). Ví dụ: window surface sử dụng bộ nhớ dùng chung giữa ứng dụng và screen compositor...

Allocate and reclaim app memory

Dalvik heap bị giới hạn cho mỗi process ứng dụng. Điều này xác định rằng kích thước hợp lý (logical size) của heap có thể tăng theo nhu cầu nhưng chỉ đến giới hạn mà hệ thống xác định cho mỗi ứng dụng.

Logical size của heap không giống với dung lượng bộ nhớ vật lý (physical memory) được sử dụng bởi heap. Khi kiểm tra heap của ứng dụng của bạn, Android sẽ tính toán một giá trị được gọi là kích thước cài đặt theo tỷ lệ (Proportional Set Size - PSS), gồm cả các dirtyclean page được chia sẻ với các process khác, nhưng chỉ với số lượng tỷ lệ với số lượng ứng dụng chia sẻ RAM đó. Tổng số (PSS) này là những gì mà hệ thống coi là physical memory footprint.

Dalvik heap không nén (compact) logical size của heap, có nghĩa là Android không giảm phân mảnh heap để dồn không gian bộ nhớ (defragment) . Android chỉ có thể thu nhỏ kích thước heap hợp lý khi có không gian chưa sử dụng ở cuối heap. Tuy nhiên, hệ thống vẫn có thể giảm bộ nhớ vật lý được sử dụng bởi heap. Sau khi thu gom rác, Dalvik đi qua heap và tìm các trang không sử dụng, sau đó trả lại các trang đó cho kernel bằng cách sử dụng madvise. Vì vậy, các paired allocations and deallocations của các chunk lớn sẽ dẫn đến việc lấy lại tất cả (hoặc gần như tất cả) bộ nhớ vật lý được sử dụng. Tuy nhiên, việc lấy lại bộ nhớ từ các cấp phát nhỏ có thể kém hiệu quả hơn nhiều vì trang được sử dụng cho cấp phát nhỏ vẫn có thể được chia sẻ với một thứ khác chưa được giải phóng.

Restrict app memory

Để duy trì môi trường đa tác vụ, Android đặt giới hạn cứng cho kích thước heap cho mỗi ứng dụng. Giới hạn kích thước heap chính xác khác nhau giữa các thiết bị dựa trên tổng lượng RAM mà thiết bị có sẵn. Nếu ứng dụng của bạn đã đạt đến dung lượng heap và cố gắng cấp phát thêm bộ nhớ, ứng dụng có thể bị OutOfMemoryError.

Trong một số trường hợp, bạn có thể muốn truy vấn hệ thống để xác định chính xác dung lượng heap bạn có trên thiết bị hiện tại, ví dụ, để xác định có bao nhiêu dữ liệu an toàn để giữ trong bộ đệm (cache). Bạn có thể truy vấn hệ thống bằng cách gọi getMemoryClass(). Method này trả về một số nguyên cho biết số megabyte có sẵn cho heap ứng dụng của bạn.

Switch apps

Khi người dùng chuyển đổi giữa các ứng dụng, Android sẽ giữ các ứng dụng không phải là foreground - tức ứng dụng không visible cho người dùng hoặc chạy foreground service trước như phát nhạc - trong một bộ đệm least-recently used (LRU) cache. Ví dụ: khi người dùng lần đầu tiên khởi chạy một ứng dụng, một process được tạo cho nó; nhưng khi người dùng rời khỏi ứng dụng, process đó không thoát. Hệ thống giữ cho quá trình lưu trữ. Nếu người dùng sau đó quay lại ứng dụng, hệ thống sẽ sử dụng lại process, do đó làm cho ứng dụng chuyển đổi nhanh hơn.

Nếu ứng dụng của bạn có một process được lưu trong bộ nhớ cache và nó vẫn giữ bộ nhớ mà hiện tại nó không cần, thì ngay cả khi người dùng không sử dụng nó, vẫn ảnh hưởng đến hiệu suất chung của hệ thống. Khi hệ thống sắp hết bộ nhớ, nó sẽ hủy các process trong LRU cache, bắt đầu với process ít được sử dụng gần đây nhất. Hệ thống cũng chiếm các process giữ nhiều bộ nhớ nhất và có thể chấm dứt chúng để giải phóng RAM.

Lưu ý: Khi hệ thống bắt đầu hủy các process trong LRU cache, nó chủ yếu hoạt động từ dưới lên. Hệ thống cũng xem xét các process nào tiêu thụ nhiều bộ nhớ hơn và do đó cung cấp cho hệ thống tăng thêm bộ nhớ nếu bị hủy. Ứng dụng của bạn càng tiêu thụ ít bộ nhớ trong danh sách LRU, càng có nhiều cơ hội ở lại danh sách và có thể nhanh chóng resume.

Memory allocation among processes

Android platform chạy trên tiền đề rằng "Bộ nhớ trống là bộ nhớ bị lãng phí" (free memory is wasted memory). Nó cố gắng sử dụng toàn bộ memory có sẵn mọi lúc. Ví dụ: hệ thống sẽ giữ các ứng dụng trong bộ nhớ sau khi chúng bị đóng để người dùng có thể nhanh chóng quay lại. Vì lý do này, các thiết bị Android thường chạy với rất ít bộ nhớ trống. Quản lý bộ nhớ là rất quan trọng để phân bổ bộ nhớ hợp lý giữa các tiến trình hệ thống (system process) quan trọng và các ứng dụng người dùng.

Types of memory

Thiết bị Android chứa ba loại bộ nhớ khác nhau: RAM, zRAMstorage. Lưu ý rằng cả CPUGPU đều truy cập cùng RAM. Types of memory

  1. RAM là loại bộ nhớ nhanh nhất, nhưng thường bị giới hạn về kích thước. Các thiết bị cao cấp thường có dung lượng RAM lớn.
  2. zRAM là một phân vùng RAM được sử dụng cho không gian trao đổi (swap space). Mọi thứ được nén lại khi được đặt vào zRAM, và sau đó được giải nén khi sao chép ra khỏi zRAM. Phần RAM này tăng hoặc giảm kích thước khi các trang bộ nhớ được chuyển vào hoặc lấy ra khỏi zRAM. Các hãng sản xuất thiết bị có thể đặt kích thước tối đa cho zRAM.
  3. Storage chứa tất cả các dữ liệu ổn định (persistent data) như file hệ thống và tất cả các ứng dụng, thư viện và platform. Storage có dung lượng lớn hơn nhiều so với hai loại bộ nhớ còn lại. Trên Android, Storage không được sử dụng để swap space như trên các triển khai Linux khác vì việc writing thường xuyên có thể gây hao mòn bộ nhớ này và rút ngắn tuổi thọ nó.

Memory pages

RAM được chia thành các trang (page). Thông thường mỗi trang là 4KB bộ nhớ.

  1. Cached: Bộ nhớ được hỗ trợ bởi một file trên storage (ví dụ: code hoặc memory-mapped files). Có hai loại bộ nhớ đệm:

    • Private: Được sở hữu bởi một process và không được chia sẻ.
      • Clean: Bản sao chưa sửa đổi của file trên storage, có thể bị xóa bởi kswapd(kernel swap daemon) để tăng bộ nhớ trống.
      • Dirty: Bản sao sửa đổi của file trên storage, có thể được di chuyển đến hoặc nén trong zRAM bằng kswapd để tăng bộ nhớ trống.
    • Shared: Được sử dụng bởi nhiều process
      • Clean: Bản sao chưa sửa đổi của file trên storage, có thể bị xóa bởi kswapd để tăng bộ nhớ trống.
      • Dirty: Bản sao sửa đổi của file trên storage, cho phép các thay đổi được ghi lại vào file trong storage để tăng bộ nhớ trống bằng kswapd, hoặc sử dụng tường minh bằng cách sử dụng msync() hoặc munmap().
  2. Anonymous: (Ẩn danh) Bộ nhớ không được hỗ trợ bởi một file trên storage (ví dụ: được cấp phát bởi mmap() với flag MAPANONYMOUS)

    • Dirty: Có thể được di chuyển / nén trong zRAM bởi kswapd để tăng bộ nhớ trống

Lưu ý: Các clean page chứa một bản sao chính xác của một file (hoặc một phần của file) tồn tại trong storage. Một clean page sẽ trở thành một dirty page khi nó không còn chứa một bản sao chính xác của file nữa (ví dụ: từ kết quả của một hoạt động ứng dụng). Các clean page có thể bị xóa vì chúng luôn có thể được tạo lại bằng cách sử dụng dữ liệu từ storage; dirty page không thể bị xóa nếu không dữ liệu sẽ bị mất.

Tỷ lệ của các free pageused page thay đổi theo thời gian khi hệ thống chủ động quản lý RAM.

Low memory management

Android có hai cơ chế chính để xử lý các tình huống bộ nhớ thấp: kernel swap daemonlow-memory killer.

kernel swap daemon

Kernel swap daemon (kswapd) là một phần của Linux kernel , chuyển đổi bộ nhớ đã sử dụng thành bộ nhớ trống. Daemon sẽ active khi bộ nhớ trống trên thiết bị sắp hết. Linux kernel duy trì một ngưỡng bộ nhớ trống mức thấp và cao. Khi bộ nhớ trống giảm xuống dưới ngưỡng thấp, kswapd bắt đầu lấy lại bộ nhớ. Khi bộ nhớ trống đạt đến ngưỡng cao, kswapd sẽ ngừng lấy lại bộ nhớ.

kswapd có thể lấy lại các clean page bằng cách xóa chúng vì chúng được backup bởi storage và chưa được sửa đổi. Nếu một process cố gắng address một clean page đã bị xóa, hệ thống sẽ sao chép page từ storage sang RAM. Hoạt động này được gọi là phân trang nhu cầu (demand paging).

kswapd có thể di chuyển các private dirty page được lưu trong bộ nhớ cache và các anonymous dirty page sang zRAM, nơi chúng được nén. Làm như vậy sẽ giải phóng bộ nhớ khả dụng trong RAM (free page). Nếu một process cố gắng chạm vào một dirty page trong zRAM, page đó sẽ không bị nén và được chuyển ngược trở lại vào RAM. Nếu process liên kết với một page nén bị kill, thì page đó sẽ bị xóa khỏi zRAM.

Nếu bộ nhớ trống giảm xuống dưới một ngưỡng nhất định, hệ thống sẽ bắt đầu hủy các process.

Low-memory killer

Nhiều lần, kswapd không thể giải phóng đủ bộ nhớ cho hệ thống. Trong trường hợp này, hệ thống sử dụng onTrimMemory() để thông báo cho ứng dụng rằng bộ nhớ sắp hết và nó sẽ giảm việc phân bổ. Nếu điều này là không đủ, kernel bắt đầu hủy các process để giải phóng bộ nhớ. Nó sử dụng low-memory killer (LMK) để làm điều này.

Để quyết định process nào bị hủy, LMK sử dụng điểm "hết bộ nhớ" ("out of memory" score) được gọi là oom_adj_score để ưu tiên các process đang chạy. Các process với số điểm cao bị hủy đầu tiên. Các ứng dụng nền bị hủy trước tiên và các process hệ thống sẽ bị hủy cuối cùng. Bảng sau liệt kê các loại điểm LMK từ cao đến thấp. Các item trong mục có điểm cao nhất, ở hàng một, sẽ bị hủy trước:

  • Background apps: Ứng dụng đã chạy trước đây và hiện không hoạt động. LMK trước tiên sẽ hủy các ứng dụng nền có oom_adj_score cao nhất.
  • Previous app: Ứng dụng nền được sử dụng gần đây nhất. Ứng dụng trước có mức độ ưu tiên cao hơn (điểm thấp hơn) so với ứng dụng chạy nền vì nhiều khả năng người dùng sẽ chuyển sang ứng dụng này hơn là một trong những ứng dụng chạy nền.
  • Home app: Đây là ứng dụng launcher. Hủy cái này sẽ làm cho hình nền biến mất.
  • Services: Service được start bởi các ứng dụng và có thể bao gồm syncing hoặc uploading lên cloud.
  • Perceptible app: Các ứng dụng non-foreground mà người dùng có thể cảm nhận được bằng một cách nào đó, chẳng hạn như process tìm kiếm hiển thị một UI nhỏ hoặc process nghe nhạc.
  • Foreground app: Ứng dụng hiện đang được sử dụng. Hủy ứng dụng nền trước trông giống như một crash ứng dụng có thể cho người dùng biết rằng có sự cố xảy ra với thiết bị.
  • Persistent (services): Đây là những core service cho thiết bị, như telephonywifi.
  • System: Các process của hệ thống. Khi các process này bị hủy, điện thoại có thể xuất hiện hiện tượng khởi động lại.
  • Native: Các low-level process được sử dụng bởi hệ thống (ví dụ: kswapd).

Các hãng sản xuất thiết bị có thể thay đổi hành vi của LMK.

Calculating memory footprint

Kernel theo dõi tất cả các trang bộ nhớ (memory page) trong hệ thống.

Khi xác định dung lượng bộ nhớ đang được sử dụng bởi một ứng dụng, hệ thống phải tính đến các trang được chia sẻ (shared page). Các ứng dụng truy cập cùng service hoặc library sẽ được chia sẻ các trang bộ nhớ. Ví dụ: Google Play Services và ứng dụng trò chơi có thể đang chia sẻ location service. Điều này gây khó khăn cho việc xác định dung lượng bộ nhớ của service lớn so với mỗi ứng dụng.

Để xác định dung lượng bộ nhớ cho một ứng dụng, có thể sử dụng bất kỳ số liệu nào sau đây:

  • Resident Set Size (RSS) (Kích thước cài đặt thường trú): Số lượng trang được chia sẻ và không chia sẻ được sử dụng bởi ứng dụng
  • Proportional Set Size (PSS) (Kích thước cài đặt theo tỷ lệ): Số lượng trang không chia sẻ được sử dụng bởi ứng dụng và phân phối đồng đều các trang được chia sẻ (ví dụ: nếu ba process đang chia sẻ 3MB, mỗi process sẽ có 1 MB trong PSS)
  • Unique Set Size (USS) (Kích thước cài đặt duy nhất): Số lượng trang không chia sẻ được sử dụng bởi ứng dụng (không bao gồm các trang được chia sẻ)

PSS rất hữu ích cho hệ điều hành khi nó muốn biết bao nhiêu bộ nhớ được sử dụng bởi tất cả các process kể từ khi các trang không được được tính nhiều lần. PSS mất nhiều thời gian để tính toán vì hệ thống cần xác định trang nào được chia sẻ và bởi bao nhiêu process.

RSS không phân biệt giữa các trang được chia sẻ và không chia sẻ (giúp tính toán nhanh hơn) và tốt hơn để theo dõi các thay đổi trong việc phân bổ bộ nhớ.

References:

https://developer.android.com/topic/performance/memory-overview

https://developer.android.com/topic/performance/memory-management

https://android.googlesource.com/platform/system/core/+/master/lmkd/README.md