クイック評価ツール

メモリ使用量を調査する必要がある場合は、まず、高レベルの評価ツールを使用して、どのカテゴリのメモリが最も重要であるかを特定します。

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 にどのように表示されるかを示します。

  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)] ボタンを 3 回タップします。

  4. 変更を確認する: meminfo -s を再度実行します。Java ヒープの値が約 30,000 KB(30 MB)増加していることがわかります。

  5. ネイティブ メモリを割り当てる: [Allocate Native Memory (1GB Incompressible)] ボタンを 1 回タップします。

  6. 変更を確認する: meminfo -s を再度実行します。[Native Heap] の値が大幅に増加(約 1,000,000 KB)しているはずです。

  7. 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 を使用した特集

特定のメモリ マッピングを特定し、クリーンメモリとダーティメモリを区別する方法について説明します。

  1. Mmap Clean Memory: MemoryLab アプリの [Mmap Clean Anon Memory (10MB)] ボタンを 1 回タップします。
  2. プロセス ID を取得する: アプリの UI に PID が表示されます。次のコマンドでも確認できます。

    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 KB の匿名マッピングが表示されます。このマップの private dirty 値は 0 である可能性が高いことに注意してください。これは「クリーン」な匿名メモリです。mmap を使用して仮想空間を予約しましたが、まだ書き込みを行っていないため、物理 RAM は実際には消費されていません。

  5. ネイティブ ヒープの比較: meminfo の演習のステップ 5 の後に実行した場合に showmap に表示される [anon:scudo:primary] マッピングと比較します。この 1 GB のネイティブ割り当ては実際にはすべてのページに書き込まれるため、これらのページは「ダーティ」であり、[private dirty] 列に大量に表示されます。

  6. 上級者向けのヒント(大きなオブジェクト): 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)がバックグラウンド プロセスの終了を開始するため、デバイスの動作が遅く感じられます。次に、パッケージごとの統計情報を確認して、デバイスがこれらのストレス状態にあるときに、特にメモリ使用量が多いアプリを特定できます。


← コンセプト | ↑ 上へ | Java →