Công cụ đánh giá nhanh

Khi bạn cần điều tra mức sử dụng bộ nhớ, hãy bắt đầu bằng các công cụ đánh giá cấp cao để xác định danh mục bộ nhớ nào quan trọng nhất.

dumpsys meminfo

Cách phổ biến nhất để xem thông tin tổng quan về mức sử dụng bộ nhớ của ứng dụng là dumpsys meminfo. Để xem thông tin chi tiết bao gồm tất cả các mục ánh xạ bộ nhớ, bạn nên thêm cờ -a:

adb shell dumpsys meminfo -a <package_name_or_pid>

Dưới đây là kết quả mẫu của một ứng dụng chịu áp lực bộ nhớ (lưu ý SwapPss cao):

** MEMINFO in pid 4562 [com.android.memorylab] **
                   Pss  Private  Private  SwapPss      Rss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty    Total     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------   ------
  Native Heap      394      268       64    17440     4264    29416    18128     8040
  Dalvik Heap      165        8        0      672     8196    40949    32757     8192
 Dalvik Other       47       28        0      604     1132
        Stack      112      108        4      312      120
...
        TOTAL    33197      580      788    21920   167776    70365    50885    16232

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:       12                          32608
         Native Heap:      268                           4264
                Code:      724                         125672
               Stack:      108                            120
            Graphics:        0                              0
       Private Other:      256
              System:    31829
             Unknown:                                    5112

           TOTAL PSS:    33197            TOTAL RSS:   167776       TOTAL SWAP PSS:    21920

 Objects
               Views:       15         ViewRootImpl:        1
         AppContexts:        5           Activities:        1
...

Diễn giải dữ liệu

  • Pss Total: Kích thước cài đặt theo tỷ lệ. Đây là tổng của Private Dirty
    • Private Clean + phần bộ nhớ công bằng của quy trình được chia sẻ với các quy trình khác (như hình ảnh khởi động Zygote). Đây là chỉ số tốt nhất cho biết "ứng dụng này chịu trách nhiệm cho bao nhiêu bộ nhớ".
  • Private Dirty: Đây là RAM mà chỉ quy trình của bạn đang sử dụng và đã được sửa đổi (hoặc là bộ nhớ ẩn danh). Đây là con số quan trọng nhất để tìm rò rỉ vì không thể loại bỏ bộ nhớ này.
  • Private Clean: RAM mà chỉ quy trình của bạn đang sử dụng, nhưng là bản sao chưa sửa đổi của một tệp trên bộ nhớ (như tệp DEX). Hệ điều hành có thể loại bỏ trạng thái này nếu bộ nhớ sắp hết.
  • SwapPss: Bộ nhớ đã được nén và hoán đổi vào ZRAM. Điều này có thể xảy ra do hệ thống đang chịu áp lực về bộ nhớ hoặc do Android Runtime đã xác định được các tiến trình nền cần thu gọn (di chuyển các trang không dùng đến hoặc không hoạt động sang ZRAM). Trong ví dụ về dumpsys, hầu hết Native Heap và Dalvik Heap đã được chuyển sang ZRAM.
  • Tóm tắt về ứng dụng: Phân loại ở cấp cao hơn, kết hợp các hàng chi tiết thành các nhóm dễ hiểu.
  • Đối tượng: Hữu ích cho việc theo dõi các thực thể Activities và ViewRootImpl bị rò rỉ. Nếu bạn thoát khỏi ứng dụng và buộc GC, thì số lượng Activities sẽ trở về 0.

Mục ngân sách bộ nhớ

Trong Android 18 trở lên, dumpsys meminfo cũng in phần Ngân sách bộ nhớ khi kết xuất một ứng dụng. Phần này đọc số liệu thống kê memcg của quy trình và của toàn bộ ứng dụng (gói), vì vậy, bạn không cần phải tự tìm các tệp cgroup. Sau đây là phần của ứng dụng mẫu MemoryLab ở nền trước, sau 3 lần nhấn vào Allocate Java Memory (10MB) (Phân bổ bộ nhớ Java (10 MB)):

 Memory Budget
                        Process(KB)       Package(KB)
                        -----------       -----------
  Current Usage:              83820             87976
  Limit:                       none           4718592
  Limit Source:                none     MemoryLimiter
  Peak Usage:                 85136             89292
  Swap Used:                      0                 0

  Usage Breakdown
    Anonymous:                81700             81700
    File:                        96              4200
    File Mapped:                 16               616
    Shared Memory:               16                16
    Kernel:                    1580              1632
    Page Tables:                640               640
    Slab:                       439               458

  Pressure & Reclaim
    Throttle Events:              0                 0
    Refaults (Anon):              0                 0
    Refaults (File):              0                 0
  • Mức sử dụng hiện tại: Bộ nhớ được tính vào memcg (memory.current). Đây là số lượng mà ngân sách bộ nhớ giới hạn. Trong mẫu, giá trị này tăng khoảng 36.700 KB sau ba lần phân bổ Java 10 MB, phù hợp với TOTAL PSS (TỔNG PSS). Tổng số gói cao hơn tổng số quy trình ngay cả đối với ứng dụng một quy trình khi các trang tệp đã lưu vào bộ nhớ đệm từ lần chạy trước chưa được thu hồi.
  • Giới hạn: Ngưỡng thu hồi (memory.high) áp dụng ngay bây giờ hoặc none nếu không có ngưỡng thu hồi. Trong mẫu, gói có giới hạn bộ nhớ hệ thống và quy trình không có giới hạn riêng.
  • Nguồn giới hạn: Nguồn gốc của giới hạn. AndroidManifest là một <memory-budget> được khai báo trong tệp kê khai ứng dụng, MemoryBudgetManager là ngân sách mà ứng dụng đặt tại thời gian chạy, MemoryLimiter là giới hạn bộ nhớ hệ thống và am memory-limiter là chế độ ghi đè thủ công được đặt bằng adb.
  • Mức sử dụng cao nhất: Mức phí cao nhất kể từ khi memcg được tạo (memory.peak). Giá trị này cho thấy N/A trên các hạt nhân không hỗ trợ.
  • Bộ nhớ hoán đổi đã sử dụng: Bộ nhớ được nén vào ZRAM (memory.swap.current).
  • Usage Breakdown (Thông tin chi tiết về mức sử dụng): Các trường chính của memory.stat, chẳng hạn như bộ nhớ ẩn danh và bộ nhớ được hỗ trợ bằng tệp.
  • Áp lực và thu hồi: Sự kiện điều tiết đếm số lần memcg vượt quá giới hạn memory.high. Lỗi trang lặp lại tính số trang đã được hoán đổi hoặc loại bỏ rồi sau đó lại cần đến. Nếu các con số này tăng lên trong khi bạn sử dụng ứng dụng, thì giới hạn sẽ chặt chẽ hơn so với tập hợp đang hoạt động của ứng dụng.

Với cờ -a hoặc -v, phần này cũng liệt kê mọi bộ đếm thô từ memory.stat và memory.events.

Bài tập thực hành: phân loại nhanh bằng meminfo

Bài tập này cho thấy cách các loại phân bổ khác nhau xuất hiện trong meminfo.

  1. Mở ứng dụng:

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. Kiểm tra bộ nhớ cơ sở:

    adb shell dumpsys meminfo -s com.android.memorylab
    

    (Cờ -s cung cấp thông tin tóm tắt ngắn gọn về các danh mục bộ nhớ).

  3. Allocate Java Memory (Phân bổ bộ nhớ Java): Nhấn vào nút Allocate Java Memory (10MB) (Phân bổ bộ nhớ Java (10 MB)) 3 lần trong ứng dụng.

  4. Quan sát thay đổi: Chạy lại meminfo -s. Bạn sẽ thấy giá trị Java Heap (Vùng nhớ khối xếp Java) tăng khoảng 30.000 KB (30 MB).

  5. Allocate Native Memory (Phân bổ bộ nhớ gốc): Nhấn vào nút Allocate Native Memory (1GB Incompressible) (Phân bổ bộ nhớ gốc (1GB không nén được)) một lần.

  6. Quan sát thay đổi: Chạy lại meminfo -s. Giá trị Native Heap (Vùng nhớ khối xếp gốc) giờ đây sẽ tăng lên đáng kể (~1.000.000 KB).

  7. So sánh với memcg (Android 18 trở lên): Xem phần Memory Budget (Ngân sách bộ nhớ) ở cuối kết quả của meminfo -s. Mức sử dụng hiện tại tăng lên cùng với TỔNG PSS sau mỗi lần phân bổ, vì cả mức phân bổ Java và mức phân bổ gốc đều được tính vào memcg của ứng dụng.

Chế độ xem toàn cầu

Để xem mức sử dụng bộ nhớ trên toàn bộ hệ thống, hãy chạy lệnh:

adb shell dumpsys meminfo

Lệnh này cung cấp thông tin tóm tắt về mức sử dụng RAM, bao gồm tổng RAM, RAM trống và danh sách các quy trình được sắp xếp theo PSS.

Các chỉ số chính của hệ thống

  • ZRAM: Hoán đổi nén trong RAM.
  • DMA-BUF: Đồ hoạ và vùng đệm bộ nhớ dùng chung. Các phiên bản Android hiện đại sử dụng DMA-BUF rộng rãi để kết xuất giao diện người dùng. Nếu tổng số DMA-BUF rất cao, hãy điều tra các quy trình có đồ hoạ nặng bằng cách sử dụng /sys/kernel/debug/dma_buf/bufinfo.
  • RAM bị mất: Bộ nhớ không được nhân hệ điều hành hoặc bất kỳ quy trình nào tính đến (ví dụ: vùng dành riêng cho phần cứng).

số liệu thống kê về memcg

Mỗi quy trình ứng dụng đều có nhóm kiểm soát bộ nhớ (memcg) riêng và nhân hệ điều hành sẽ liên tục tính tổng bộ nhớ được tính phí cho nhóm đó (xem memcg để biết cách lưu giữ hồ sơ này khác với RSS và PSS như thế nào). Việc đọc các tệp này không tốn nhiều tài nguyên và không làm gián đoạn ứng dụng, vì vậy, bạn có thể lấy mẫu mỗi giây trong khi chạy một luồng người dùng.

Tra cứu đường dẫn memcg của một quy trình từ dòng 0:: của /proc/<PID>/cgroup thay vì tự tạo đường dẫn, vì bố cục cgroup có thể khác nhau giữa các thiết bị. Truyền từng lệnh thiết bị đến adb shell dưới dạng một chuỗi được trích dẫn để vỏ thiết bị thấy dấu ngoặc kép:

PID=$(adb shell pidof -s <package_name>)

CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")

adb shell "cat $CG/memory.current $CG/memory.swap.current"

adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

Các tệp chính là:

  • memory.current: Bộ nhớ được tính cho quy trình, tính bằng byte. Đây là số lượng mà ngân sách bộ nhớ giới hạn.
  • memory.swap.current: Bộ nhớ được nén vào ZRAM, tính bằng byte.
  • memory.stat: Thông tin chi tiết về mức phí, tính theo byte cho kích thước. anon là bộ nhớ ẩn danh, chẳng hạn như vùng nhớ khối xếp Java và vùng nhớ khối xếp gốc. file là bộ nhớ đệm trang được hỗ trợ bằng tệp, chẳng hạn như mã và tài nguyên. workingset_refault_anon và workingset_refault_file đếm số trang đã được hoán đổi hoặc loại bỏ và sau đó cần dùng lại. Số lượng lỗi tham chiếu tăng lên có nghĩa là quy trình này sắp hết bộ nhớ.
  • memory.events: Số lượng sự kiện giới hạn bộ nhớ. high đếm số lần quy trình bị điều tiết do vượt quá giới hạn memory.high.

Thư mục mẹ của memcg quy trình thuộc về toàn bộ ứng dụng (UID của ứng dụng). Các tệp của nó bao gồm bộ nhớ của tất cả các quy trình đang chạy của ứng dụng, cộng với các trang tệp được lưu vào bộ nhớ đệm còn sót lại từ các quy trình trước đó của ứng dụng đã thoát và chưa được thu hồi:

adb shell "cat $CG/../memory.current"

Nếu bạn gặp lỗi về quyền, hãy chạy adb root rồi thử lại. Một số thiết bị chỉ cho phép bạn đọc các tệp này dưới dạng thư mục gốc.

Để xem hướng dẫn từng bước về các tệp này bằng ứng dụng mẫu MemoryLab, hãy xem phần Bài tập thực hành: khám phá memcg.

showmap

Để xem thông tin chi tiết hơn về ánh xạ bộ nhớ của một quy trình, hãy sử dụng showmap. Công cụ này đọc từ /proc/<pid>/smaps.

adb shell showmap <pid>

showmap liệt kê mọi bản đồ bộ nhớ trong quy trình. Các mối liên kết khoá cần tìm bao gồm:

  • [anon:scudo:primary] hoặc [anon:libc_malloc]: Vùng nhớ khối xếp gốc. Các phiên bản Android hiện đại sử dụng trình phân bổ Scudo.
  • *.art: Hình ảnh vùng nhớ khối xếp Java (ví dụ: hình ảnh khởi động).
  • [anon:dalvik-main space]: Vùng nhớ khối xếp Java chính.
  • *.so: Thư viện chia sẻ.
  • *.{oat,dex,odex,vdex}: Mã DEX đã biên dịch.

Bạn có thể sắp xếp kết quả để tìm ra những đối tượng tiêu thụ bộ nhớ riêng tư lớn nhất:

# Sort by private clean + private dirty memory
adb shell showmap <pid> | awk '{print $6+$7, $0}' | sort -rn | head -n 10

Kết quả mẫu:

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
-------- -------- -------- -------- -------- -------- -------- -------- -------- --------- --------- --------- -------- -------- -------- ---- ------------------------------
2776    18176     5784     2823        0     3008      308     2468     4700     4700         0         0         0        0        0        0   47 [anon:scudo:primary]
560      952      876      657      316        0      560        0       12       12         0         0         0        0        0        0    8 /data/app/.../base.apk
448   393216      448      448        0        0        0      448        0        0         0         0         0        0        0        0    2 [anon:dalvik-main space]
...

Bài tập thực hành: tìm hiểu sâu về showmap

Tìm hiểu cách xác định các mối liên kết bộ nhớ cụ thể và phân biệt giữa bộ nhớ sạch và bộ nhớ bẩn.

  1. Mmap Clean Memory (Bộ nhớ sạch Mmap): Nhấn vào nút Mmap Clean Anon Memory (10MB) (Bộ nhớ ẩn danh sạch Mmap (10 MB)) trong ứng dụng MemoryLab một lần.
  2. Lấy mã định danh tiến trình: Giao diện người dùng của ứng dụng cho biết mã định danh tiến trình của ứng dụng. Bạn cũng có thể tìm thấy tệp này bằng lệnh sau:

    adb shell pidof com.android.memorylab
    
  3. Kiểm tra bằng showmap:

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. Xác định mối liên kết: Bạn sẽ thấy một mối liên kết ẩn danh có nhãn [anon:memorylab-clean] với kích thước ảo chính xác là 10240 KB. Lưu ý rằng giá trị private dirty cho bản đồ này có thể là 0. Đây là bộ nhớ ẩn danh "sạch" vì chúng ta đã sử dụng mmap để đặt trước không gian ảo nhưng chưa ghi vào đó (vì vậy, không có RAM vật lý nào thực sự được sử dụng).

  5. So sánh vùng nhớ khối xếp gốc: So sánh vùng nhớ này với các liên kết [anon:scudo:primary] sẽ xuất hiện trong showmap nếu bạn chạy vùng nhớ này sau bước 5 của bài tập meminfo. Vì mức phân bổ gốc 1 GB đó thực sự ghi vào mọi trang, nên các trang đó là "bị bẩn" và sẽ xuất hiện nhiều trong cột private dirty (bị bẩn riêng tư).

  6. Mẹo nâng cao (Đối tượng lớn): Lưu ý các lượt phân bổ Vùng nhớ khối xếp Java trong bài tập meminfo. Vì là mảng byte 10 MB, nên các mảng này có thể xuất hiện trong showmap dưới dạng [anon:dalvik-free list large object space]. Android Runtime (ART) đặt các đối tượng lớn hơn 12 KB vào một Không gian đối tượng lớn (LOS) riêng biệt để tránh phân mảnh trong vùng nhớ khối xếp chính.

procstats

Trong khi meminfo cung cấp thông tin tổng quan nhanh theo thời gian, thì procstats cung cấp dữ liệu trong quá khứ bằng cách lấy mẫu định kỳ trạng thái và mức sử dụng bộ nhớ của tất cả ứng dụng đang chạy. Đây là công cụ chính dùng để theo dõi tình trạng bộ nhớ dài hạn và là công cụ thúc đẩy báo cáo mức sử dụng bộ nhớ trong phần Cài đặt Android.

# View stats for the last 3 hours
adb shell dumpsys procstats --hours 3

Tìm hiểu về các trạng thái

Kết quả của procstats có thể rất nhiều. Phần quan trọng nhất là các chỉ số trong ngoặc đơn bên cạnh mỗi trạng thái quy trình:

Top: 100% (54MB-64MB-67MB / 49MB-58MB-61MB / 110MB-120MB-130MB over 4)

Định dạng là: (% thời gian ở trạng thái) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss trên các mẫu)

  • Pss (Kích thước cài đặt theo tỷ lệ): Bộ nhớ dùng chung được chia theo tỷ lệ giữa các quy trình.
  • Uss (Kích thước tập hợp duy nhất): Bộ nhớ chỉ được sử dụng bởi quy trình này (tương đương với Private Clean + Private Dirty).
  • Rss (Kích thước cài đặt thường trú): Tổng bộ nhớ thực được sử dụng, bao gồm tất cả các trang được chia sẻ (có xu hướng tính thừa).

Lý do điều này quan trọng:

  • Ứng dụng ổn định: giá trị tối thiểu/trung bình/tối đa gần nhau.
  • Rò rỉ bộ nhớ: mức tối đa cao hơn đáng kể so với mức tối thiểu và mức trung bình tăng dần theo thời gian khi số lượng mẫu tăng lên.
  • Mức sử dụng tăng đột biến: mức tối đa cao hơn nhiều so với mức trung bình, cho thấy các hoạt động nặng tạm thời (như xử lý hình ảnh) có thể gây ra lỗi OOM nếu xảy ra không đúng thời điểm.

Trạng thái quy trình

procstats theo dõi các ứng dụng trên nhiều tiểu bang (thường được viết tắt):

  • Duy trì: Các quy trình hệ thống quan trọng luôn chạy.
  • Trên cùng: Ứng dụng hiện đang chạy trên nền trước và người dùng có thể nhìn thấy.
  • ImpFg (Nền trước quan trọng): Các tiến trình nền quan trọng (như IME) hoặc ứng dụng có dịch vụ nền trước.
  • ImpBg (Tiến trình nền quan trọng): Các tiến trình nền được coi là quan trọng đối với hệ thống.
  • Fgs / BFgs: Dịch vụ nền trước hoặc Dịch vụ nền trước liên kết.
  • Service / ServRst: Các dịch vụ nền hoặc dịch vụ hiện đang khởi động lại.
  • Cached / LastAct: Các ứng dụng được lưu vào bộ nhớ đệm trong nền để khởi động lại nhanh. Hệ thống có thể loại bỏ các quy trình này một cách an toàn.

Trạng thái bộ nhớ thiết bị và số liệu thống kê về thời gian chạy

procstats cũng theo dõi lượng thời gian mà toàn bộ thiết bị phải chịu áp lực về bộ nhớ. Tìm mục Run time Stats (Số liệu thống kê về thời gian chạy):

Run time Stats:
  SOff/Norm: +1h10m15s
   SOn/Norm: +15m20s
        Low: +2m10s
       Crit: +45s
  • SOff/SOn: Tắt màn hình so với Bật màn hình.
  • Norm / Mod / Low / Crit: Các mức áp lực bộ nhớ (Bình thường, Trung bình, Thấp, Nghiêm trọng).

Nếu một thiết bị dành nhiều thời gian ở trạng thái Thấp hoặc Nghiêm trọng, thì thiết bị đó sẽ có cảm giác chậm chạp vì nhân hệ điều hành sẽ tích cực lấy lại bộ nhớ và Trình tắt ứng dụng khi bộ nhớ thấp (LMK) bắt đầu chấm dứt các quy trình trong nền. Sau đó, bạn có thể xem số liệu thống kê theo từng gói để biết ứng dụng nào có mức sử dụng bộ nhớ cao, đặc biệt là khi thiết bị ở trạng thái căng thẳng.


← Khái niệm | ↑ Lên | Java →