Инструменты быстрой оценки

Если вам нужно проанализировать использование памяти, начните с инструментов оценки высокого уровня, чтобы определить, какие категории памяти наиболее важны.

dumpsys meminfo

Самый распространенный способ получить обзор использования памяти приложением – это dumpsys meminfo. Чтобы посмотреть подробную разбивку, включая все сопоставления памяти, часто бывает полезно включить флаг -a:

adb shell dumpsys meminfo -a <package_name_or_pid>

Ниже приведен пример выходных данных приложения, испытывающего нехватку памяти (обратите внимание на высокое значение SwapPss):

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

Интерпретация данных

  • Pss Total – пропорциональный размер набора. Это сумма Private Dirty.
    • Private Clean + доля памяти, которая используется совместно с другими процессами (например, загрузочным образом Zygote). Это лучший показатель того, сколько памяти занимает приложение.
  • Частная измененная память. Это ОЗУ, которая используется только вашим процессом и была изменена (или является анонимной памятью). Это самый важный показатель для поиска утечек, поскольку эту память нельзя освободить.
  • Частная чистая память. ОЗУ, которое используется только вашим процессом, но является неизмененной копией файла в хранилище (например, DEX-файла). ОС может удалить его, если памяти недостаточно.
  • SwapPss – память, которая была сжата и перенесена в ZRAM. Это может происходить из-за нехватки памяти или потому, что среда Android Runtime определила фоновые процессы, которые нужно сжать (переместив их неиспользуемые или неактивные страницы в ZRAM). В примере с dumpsys почти все Native Heap и Dalvik Heap были перемещены в ZRAM.
  • Сводка по приложениям. Более общая классификация, в которой подробные строки объединены в понятные группы.
  • Объекты. Полезно для отслеживания утечек экземпляров Activities и ViewRootImpl. Если вы выйдете из приложения и принудительно запустите сборку мусора, количество объектов Activities должно вернуться к нулю.

Раздел "Бюджет памяти"

В Android 18 и более поздних версиях dumpsys meminfo также выводит раздел Memory Budget, когда создает дамп одного приложения. В этом разделе содержатся статистические данные memcg для процесса и всего приложения (пакета), поэтому вам не нужно искать файлы cgroup самостоятельно. Вот как выглядит раздел из примера приложения MemoryLab на переднем плане после трех нажатий на кнопку Allocate Java Memory (10MB) (Выделить память Java (10 МБ)):

 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
  • Текущее использование. Память, выделенная для memcg (memory.current). Это значение ограничивается бюджетом памяти. В примере он увеличился примерно на 36 700 КБ после трех выделений памяти Java по 10 МБ, что соответствует TOTAL PSS. Общий объем памяти пакета превышает общий объем памяти процесса даже для приложения с одним процессом, если кэшированные страницы файлов из предыдущего запуска ещё не были освобождены.
  • Limit (Ограничение). Порог возврата (memory.high), который применяется в настоящее время, или none, если он не задан. В примере у пакета есть ограничение на системную память, а у процесса – нет.
  • Источник ограничения. Откуда взялось ограничение. AndroidManifest – это <memory-budget>, объявленный в манифесте приложения, MemoryBudgetManager – бюджет, заданный приложением во время выполнения, MemoryLimiter – ограничение системной памяти, а am memory-limiter – переопределение, заданное вручную с помощью adb.
  • Пиковое использование. Максимальный объем памяти, использованный с момента создания memcg (memory.peak). Если ядро не поддерживает этот параметр, показывается N/A.
  • Файл подкачки. Память, сжатая в ZRAM (memory.swap.current).
  • Разбивка по использованию. Основные поля memory.stat, такие как анонимная память и память, поддерживаемая файлами.
  • Нагрузка и освобождение памяти. События регулирования – сколько раз memcg превышал лимит memory.high. Повторные ошибки – это количество страниц, которые были выгружены или удалены, а затем снова потребовались. Если эти числа растут во время использования приложения, значит, лимит меньше, чем рабочий набор приложения.

Если вы используете флаг -a или -v, в разделе также будут перечислены все необработанные счетчики из memory.stat и memory.events.

Практическое упражнение: быстрая сортировка с помощью meminfo

Это упражнение показывает, как разные типы распределения отображаются в meminfo.

  1. Запустите приложение.

    adb shell am start -W -n com.android.memorylab/.MainActivity
    
  2. Проверьте базовый объем памяти

    adb shell dumpsys meminfo -s com.android.memorylab
    

    (Параметр -s позволяет получить краткую сводку по категориям памяти.)

  3. Выделение памяти Java. Нажмите кнопку Allocate Java Memory (10MB) (Выделить память Java (10 МБ)) три раза.

  4. Проверьте изменения. Снова запустите meminfo -s. Значение Java Heap должно увеличиться примерно на 30 000 КБ (30 МБ).

  5. Allocate Native Memory (Выделить нативную память). Нажмите кнопку Allocate Native Memory (1GB Incompressible) (Выделить нативную память (1 ГБ несжимаемой)) один раз.

  6. Проверьте изменения. Снова запустите meminfo -s. Значение Native Heap должно значительно увеличиться (примерно до 1 000 000 КБ).

  7. Сравните с memcg (Android 18 и более поздние версии). Посмотрите раздел Memory Budget (Бюджет памяти) в конце выходных данных meminfo -s. Текущее использование растет вместе с Общим PSS после каждого выделения, поскольку и Java, и нативные выделения относятся к memcg приложения.

Глобальный вид

Чтобы посмотреть использование памяти во всей системе, выполните следующую команду:

adb shell dumpsys meminfo

Вы увидите сводку по использованию ОЗУ, включая общий объем ОЗУ, объем свободной ОЗУ и список процессов, отсортированных по PSS.

Ключевые системные показатели

  • ZRAM – сжатый файл подкачки в ОЗУ.
  • DMA-BUF – графические буферы и буферы общей памяти. Современные версии Android активно используют DMA-BUF для рендеринга пользовательского интерфейса. Если значение DMA-BUF очень высокое, изучите процессы, связанные с графикой, с помощью /sys/kernel/debug/dma_buf/bufinfo.
  • Потерянная ОЗУ. Память, которая не учитывается ядром или каким-либо процессом (например, зарезервированные аппаратные области).

Статистика memcg

У каждого процесса приложения есть собственная контрольная группа памяти (memcg), и ядро ведет учет выделенной ему памяти (о том, чем этот учет отличается от RSS и PSS, читайте в статье memcg). Чтение этих файлов не требует больших ресурсов и не мешает работе приложения, поэтому вы можете считывать их каждую секунду во время выполнения пути пользователя.

Найдите путь memcg процесса в строке 0:: файла /proc/<PID>/cgroup, а не создавайте его самостоятельно, поскольку макет cgroup может отличаться на разных устройствах. Передайте каждую команду устройства в adb shell в виде одной строки в кавычках, чтобы оболочка устройства видела кавычки:

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"

Ключевые файлы:

  • memory.current – память, выделенная процессу, в байтах. Именно это значение ограничивается бюджетом памяти.
  • memory.swap.current – память, сжатая в ZRAM, в байтах.
  • memory.stat – разбивка платежа, в байтах для размеров. anon – анонимная память, например кучи Java и нативного кода. file – это кеш страниц, поддерживаемый файлами, например кодом и ресурсами. workingset_refault_anon и workingset_refault_file – количество страниц, которые были выгружены или удалены, а затем снова потребовались. Растущее количество повторных ошибок означает, что процессу не хватает памяти.
  • memory.events: количество событий, связанных с ограничением памяти. high – сколько раз процесс был ограничен из-за превышения лимита memory.high.

Родительский каталог memcg процесса принадлежит всему приложению (его UID). В его файлах содержится память всех запущенных процессов приложения, а также кешированные страницы файлов, оставшиеся от предыдущих процессов приложения, которые были завершены и ещё не восстановлены:

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

Если появится ошибка, связанная с разрешениями, выполните команду adb root и повторите попытку. Некоторые устройства позволяют читать эти файлы только от имени пользователя root.

Чтобы узнать, как работать с этими файлами, ознакомьтесь с практическим руководством по memcg, в котором используется пример приложения MemoryLab.

showmap

Чтобы получить более подробную информацию о сопоставлениях памяти процесса, используйте showmap. Этот инструмент считывает данные из /proc/<pid>/smaps.

adb shell showmap <pid>

showmap перечисляет все карты памяти в процессе. Вот на какие сопоставления клавиш стоит обратить внимание:

  • [anon:scudo:primary] или [anon:libc_malloc] – нативная куча. В современных версиях Android используется распределитель Scudo.
  • *.art – образы кучи Java (например, загрузочный образ).
  • [anon:dalvik-main space] – основная куча Java.
  • *.so – общие библиотеки.
  • *.{oat,dex,odex,vdex} – скомпилированный код DEX.

Вы можете отсортировать вывод, чтобы найти самые большие потребители частной памяти:

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

Пример результата:

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

Практическое упражнение: подробный анализ с помощью showmap

Узнайте, как определить конкретные сопоставления памяти и различить чистую и грязную память.

  1. Mmap Clean Memory. Нажмите кнопку Mmap Clean Anon Memory (10MB) в приложении MemoryLab один раз.
  2. Получите идентификатор процесса. Идентификатор процесса указан в интерфейсе приложения. Вы также можете найти его с помощью этой команды:

    adb shell pidof com.android.memorylab
    
  3. Проверка с помощью showmap

    # Replace <PID> with the actual process ID from the previous step
    adb shell showmap <PID> | grep "memorylab-clean"
    
  4. Найдите сопоставление. Вы должны увидеть анонимное сопоставление с меткой [anon:memorylab-clean] и виртуальным размером ровно 10 240 КБ. Обратите внимание, что значение private dirty для этой карты, скорее всего, равно 0. Это "чистая" анонимная память, поскольку мы использовали mmap, чтобы зарезервировать виртуальное пространство, но ещё ничего в него не записали (поэтому физическая ОЗУ не используется).

  5. Сравнение нативных куч. Сравните это с сопоставлениями [anon:scudo:primary], которые появились бы в showmap, если бы вы запустили его после шага 5 упражнения meminfo. Поскольку этот 1 ГБ нативной памяти фактически записывается на каждую страницу, эти страницы являются "грязными" и будут отображаться в столбце private dirty.

  6. Совет (крупные объекты). Обратите внимание на выделение памяти в куче Java из упражнения meminfo. Поскольку это массивы байтов размером 10 МБ, в showmap они, скорее всего, будут представлены как [anon:dalvik-free list large object space]. Android Runtime (ART) помещает объекты размером более 12 КБ в отдельное пространство для больших объектов (LOS), чтобы избежать фрагментации в основной куче.

procstats

Если meminfo предоставляет моментальный снимок, то procstats – статистику, периодически собирая данные о состоянии и использовании памяти всех запущенных приложений. Это основной инструмент для долгосрочного мониторинга состояния памяти, который используется для создания отчетов об использовании памяти в настройках Android.

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

Что означают статусы

Результаты работы procstats могут быть слишком объемными. Самая важная часть – это показатели в скобках рядом с каждым состоянием процесса:

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

Формат: (% времени в состоянии) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss по выборкам)

  • Pss (Proportional Set Size) – общая память делится пропорционально между процессами.
  • Uss (Unique Set Size) – память, используемая только этим процессом (эквивалентна Private Clean + Private Dirty).
  • Rss (Resident Set Size) – общий объем используемой физической памяти, включая все общие страницы (может быть завышен).

Почему это важно

  • Стабильное приложение. Минимальное, среднее и максимальное значения близки друг к другу.
  • Утечка памяти. Максимальное значение значительно выше минимального, а среднее значение со временем растет по мере увеличения количества выборок.
  • Скачкообразное использование. Максимальное значение намного выше среднего. Это указывает на временные тяжелые операции (например, обработку изображений), которые могут привести к ошибкам нехватки памяти, если они происходят в неподходящее время.

Состояния процесса

procstats отслеживает приложения в нескольких штатах (часто сокращенно):

  • Persist. Критически важные системные процессы, которые выполняются постоянно.
  • Top. Приложение находится на переднем плане и видно пользователю.
  • ImpFg (важные активные службы) – критически важные фоновые процессы (например, IME) или приложения с активной службой.
  • ImpBg (Important Background) – фоновые процессы, которые считаются важными для системы.
  • Fgs/BFgs – активные службы или связанные активные службы.
  • Service/ServRst – фоновые службы или службы, которые перезапускаются.
  • Cached/LastAct – приложения, которые кешируются в фоновом режиме для быстрого перезапуска. Их можно безопасно завершить.

Состояние памяти устройства и статистика времени работы

procstats также отслеживает, сколько времени все устройство работало в условиях нехватки памяти. Найдите раздел Run time Stats (Статистика времени выполнения).

Run time Stats:
  SOff/Norm: +1h10m15s
   SOn/Norm: +15m20s
        Low: +2m10s
       Crit: +45s
  • SOff/SOn – экран выключен или включен.
  • Norm / Mod / Low / Crit – уровни нагрузки на память (нормальный, умеренный, низкий, критический).

Если устройство долго находится в состоянии Low или Crit, оно будет работать медленно, поскольку ядро активно освобождает память, а LMK начинает завершать фоновые процессы. Затем вы можете просмотреть статистику по пакетам, чтобы узнать, какие приложения активно используют память, когда устройство находится в этих состояниях.


← Основные понятия | ↑ Наверх | Java →