メモリ使用量を調査する必要がある場合は、まず、高レベルの評価ツールを使用して、どのカテゴリのメモリが最も重要であるかを特定します。
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: Proportional Set Size。これは、
Private Dirty + Private Cleanと、他のプロセス(Zygote ブートイメージなど)と共有されるプロセスの公平なメモリ共有の合計です。これは、「このアプリが使用しているメモリ量」を把握するのに最適な指標です。 - プライベート ダーティ: アプリのプロセスのみが使用し、変更された RAM(または匿名メモリ)です。このメモリはドロップできないため、リークを検出するうえで最も重要な数値です。
- プライベート クリーン: アプリのプロセスのみが使用している RAM。ただし、ストレージ上のファイルの未変更のコピー(DEX ファイルなど)。メモリが不足している場合、OS によって破棄されることがあります。
SwapPss: 圧縮されて ZRAM にスワップされたメモリ。これは、システムがメモリ不足になっているか、Android ランタイムが圧縮するバックグラウンド プロセス(未使用または非アクティブなページを ZRAM に移動する)を特定したことが原因で発生する可能性があります。
dumpsysの例では、Native HeapとDalvik Heapのほぼすべてが ZRAM に移動されています。キーポイント: [App Summary] セクションの [Java Heap] と [Native Heap] の値は、常駐(物理)メモリを表します。アプリのメモリが ZRAM に圧縮されている場合、これらの特定のサマリー値は想定よりもはるかに低くなる可能性があります。全体像を把握するには、必ず TOTAL SWAP PSS または詳細な
SwapPss列を確認してください。アプリの概要: 詳細な行をわかりやすいバケットにまとめた、より上位の分類。
オブジェクト: リークした
ActivitiesインスタンスとViewRootImplインスタンスを追跡するのに役立ちます。アプリを終了して GC を強制すると、Activitiesの数は 0 に戻ります。
メモリ予算セクション
Android 18 以降では、dumpsys meminfo は単一のアプリをダンプするときに Memory Budget セクションも出力します。このセクションでは、プロセスとアプリ全体(パッケージ)の memcg 統計情報が読み取られるため、cgroup ファイルを自分で見つける必要はありません。以下は、[Allocate Java Memory (10MB)] を 3 回タップした後の、フォアグラウンドの MemoryLab サンプルアプリのセクションです。
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)に課金されるメモリ。これは、メモリ予算で制限される数です。このサンプルでは、3 回の 10 MB の Java 割り当ての後、TOTAL PSS に沿って約 36,700 KB 増加しています。以前の実行でキャッシュに保存されたファイルページがまだ再利用されていない場合、単一プロセスのアプリであっても、パッケージの合計はプロセスの合計よりも大きくなります。 - 上限: 現在適用されている再利用しきい値(
memory.high)。存在しない場合はnone。このサンプルでは、パッケージにシステムメモリの上限があり、プロセス自体には上限がありません。 - 上限のソース: 上限のソース。
AndroidManifestはアプリ マニフェストで宣言された<memory-budget>、MemoryBudgetManagerはアプリが実行時に設定した予算、MemoryLimiterはシステムメモリの上限、am memory-limiterはadbで設定された手動オーバーライドです。 - ピーク使用量: memcg が作成されてからの最大料金(
memory.peak)。これをサポートしていないカーネルではN/Aが表示されます。 - 使用済みスワップ: ZRAM に圧縮されたメモリ(
memory.swap.current)。 - 使用量の内訳:
memory.statの主なフィールド(匿名メモリ、ファイルバック メモリなど)。 - Pressure & Reclaim: Throttle Events は、memcg が
memory.highの上限を超えた回数をカウントします。リフォールトは、スワップアウトまたは強制排除された後に再度必要になったページをカウントします。アプリの使用中にこれらの数値が増加する場合、上限はアプリのワーキング セットよりも厳しくなります。
-a フラグまたは -v フラグを使用すると、memory.stat と memory.events のすべての未加工カウンタも一覧表示されます。
演習: meminfo を使用したクイック トリアージ
この演習では、さまざまなタイプの割り当てが meminfo にどのように表示されるかを示します。
アプリを起動する:
adb shell am start -W -n com.android.memorylab/.MainActivityベースライン メモリを確認する:
adb shell dumpsys meminfo -s com.android.memorylab(
-sフラグは、メモリのカテゴリを簡潔にまとめたものです)。Java メモリを割り当てる: アプリで [Allocate Java Memory (10MB)] ボタンを 3 回タップします。
変更を確認する:
meminfo -sを再度実行します。Java ヒープの値が約 30,000 KB(30 MB)増加していることがわかります。ネイティブ メモリを割り当てる: [Allocate Native Memory (1GB Incompressible)] ボタンを 1 回タップします。
変更を確認する:
meminfo -sを再度実行します。[Native Heap] の値が大幅に増加(約 1,000,000 KB)しているはずです。memcg と比較する(Android 18 以降):
meminfo -s出力の最後にある Memory Budget セクションを確認します。Java とネイティブの両方の割り当てがアプリの memcg に課金されるため、割り当てごとに Current Usage が TOTAL PSS とともに増加します。
グローバル ビュー
システム全体のメモリ使用量を確認するには、次のコマンドを実行します。
adb shell dumpsys meminfo
これにより、合計 RAM、空き RAM、PSS で並べ替えられたプロセスのリストなど、RAM 使用量の概要が提供されます。
主要なシステム指標
- ZRAM: RAM 内の圧縮スワップ。
- DMA-BUF: グラフィックと共有メモリバッファ。最新の Android バージョンでは、UI レンダリングに DMA-BUF が広く使用されています。
DMA-BUFの合計が非常に高い場合は、/sys/kernel/debug/dma_buf/bufinfoを使用してグラフィックを多用するプロセスを調査します。 - Lost RAM: カーネルまたはプロセスで考慮されていないメモリ(ハードウェア予約領域など)。
memcg 統計情報
すべてのアプリプロセスには独自のメモリ コントロール グループ(memcg)があり、カーネルは、そのグループに割り当てられたメモリの合計を保持します(このアカウンティングが RSS や PSS とどのように異なるかについては、memcg をご覧ください)。これらのファイルの読み取りは負荷が低く、アプリの動作を妨げないため、ユーザーフローの実行中に 1 秒ごとにサンプリングできます。
パスをハードコードするのではなく、/proc/<PID>/cgroup の 0:: 行からプロセスの memcg パスを検索します。各デバイス コマンドを単一引用符で囲んだ文字列として 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"
アプリは、MemoryBudgetManager を介して、プロセス内で独自の cgroup 使用量をクエリできます。adb shell からアプリの cgroup ファイルを検査するときに権限エラーが発生した場合は、userdebug ビルドまたは Google Play のないエミュレータ イメージで adb root を実行してから、もう一度お試しください。
MemoryLab サンプルアプリでこれらのファイルを確認するガイド付きチュートリアルについては、ハンズオン演習: memcg の確認をご覧ください。
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 を使用した特集
特定のメモリ マッピングを特定し、クリーンメモリとダーティメモリを区別する方法について説明します。
- Mmap Clean Memory: MemoryLab アプリの [Mmap Clean Anon Memory (10MB)] ボタンを 1 回タップします。
プロセス ID を取得する: アプリの UI に PID が表示されます。次のコマンドでも確認できます。
adb shell pidof com.android.memorylabshowmap で検査する:
# Replace <PID> with the actual process ID from the previous step adb shell showmap <PID> | grep "memorylab-clean"マッピングを特定する:
[anon:memorylab-clean]というラベルが付いた、仮想サイズが正確に 10,240 KB の匿名マッピングが表示されます。このマップの private dirty 値は 0 である可能性が高いことに注意してください。これは「クリーン」な匿名メモリです。mmapを使用して仮想空間を予約しましたが、まだ書き込みを行っていないため、物理 RAM は実際には消費されていません。ネイティブ ヒープの比較:
meminfoの演習のステップ 5 の後に実行した場合にshowmapに表示される[anon:scudo:primary]マッピングと比較します。この 1 GB のネイティブ割り当ては実際にはすべてのページに書き込まれるため、これらのページは「ダーティ」であり、[private dirty] 列に大量に表示されます。上級者向けのヒント(大きなオブジェクト):
meminfoの演習で Java ヒープの割り当てに注目してください。これらは 10 MB のバイト配列であるため、showmapに[anon:dalvik-free list large object space]として表示される可能性があります。Android ランタイム(ART)は、メインヒープの断片化を回避するため、12 KB を超えるオブジェクトを別のラージ オブジェクト スペース(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)
形式は次のとおりです。 (% time in state) (minPss-avgPss-maxPss / minUss-avgUss-maxUss / minRss-avgRss-maxRss over samples)
- Pss(Proportional Set Size): 共有メモリはプロセス間で比例的に分割されます。
- Uss(Unique Set Size): このプロセスのみで使用されるメモリ(Private Clean + Private Dirty に相当)。
- Rss(Resident Set Size): 使用されている物理メモリの合計。すべての共有ページが含まれます(過大にカウントされる傾向があります)。
なぜ重要か:
- 安定したアプリ: 最小値、平均値、最大値が近い。
- メモリリーク: 最大値が最小値よりも大幅に高く、サンプル数が増えるにつれて平均値が徐々に増加します。
- 使用率の急増: 最大値が平均値を大幅に上回っている場合は、一時的な重いオペレーション(画像処理など)が発生していることを示しています。このようなオペレーションが不適切なタイミングで発生すると、OOM が発生する可能性があります。
プロセスの状態
procstats は、いくつかの状態(多くの場合、省略形)でアプリを追跡します。
- 永続: 常に実行されている重要なシステム プロセス。
- Top: アプリは現在フォアグラウンドにあり、ユーザーに表示されています。
- ImpFg(重要なフォアグラウンド): 重要なバックグラウンド プロセス(IME など)またはフォアグラウンド サービスを使用するアプリ。
- ImpBg(重要なバックグラウンド): システムにとって重要と見なされるバックグラウンド プロセス。
- 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: メモリ負荷レベル(Normal、Moderate、Low、Critical)。
デバイスが Low または Crit 状態に長時間留まっている場合、カーネルが積極的にメモリを再利用し、ローメモリ キラー(LMK)がバックグラウンド プロセスの終了を開始するため、デバイスの動作が遅く感じられます。次に、パッケージごとの統計情報を確認して、デバイスがこれらのストレス状態にあるときに、特にメモリ使用量が多いアプリを特定できます。