基本コンセプト

Android のメモリ使用量を把握するには、高レベルの Java オブジェクトから低レベルのカーネルページまで、さまざまな観点からシステムを調べる必要があります。

パフォーマンスの崖

メモリ使用量自体は、良いことでも悪いことでもありません。重要なのは、メモリを何に使用しているかです。ただし、デバイスで使用可能なメモリの上限に近づくと、最終的に「パフォーマンスの崖から落ちる」ことになります。

特定のメモリしきい値まで安定したパフォーマンスを示し、システムがスラッシングを開始してプロセスを強制終了すると急激に低下するグラフ

崖から遠く離れている場合、メモリ使用量を少し(50 MB など)増やしても、パフォーマンスに目立った影響はありません。ただし、使用可能な RAM が使い果たされると、崖に到達します。この時点で、オペレーティング システムはメモリのページアウトとプロセスの強制終了を開始して、スペースを解放する必要があります。この崖を超えると、メモリ使用量がわずかに増加しただけでも、重大な「スラッシング」が発生する可能性があります。スラッシングが発生すると、重要なプロセスが強制終了されるため、デバイスが応答しなくなったり、再起動しているように見えたりします。

Android スタック

Android スタックの各レイヤには、それぞれ独自のメモリビューがあります。

Android メモリー スタックのブロック図。上部に Java アプリケーション、その下に ART、下部に物理ページを含む Linux カーネルが表示されています。

  1. アプリケーション(Java / Kotlin): デベロッパーは主に、Java ヒープに割り当てられた Java オブジェクトを確認します。
  2. Android ランタイム(ART): ART は、カーネルの仮想メモリページを使用して Java / Kotlin ヒープを管理します。以前は Dalvik と呼ばれていました(これらの用語は同じ意味で使用されることがあります)。
  3. Linux カーネル: カーネルは、物理ページと仮想ページの観点からメモリを認識します。従来は 4 KB でしたが、Android では 16 KB のページサイズもサポートされています。カーネルは「Java オブジェクト」について何も知りません。

メモリを最適化するには、独自のレイヤ内で最適化するか(ビットマップの読み込みを減らすなど)、下位のレイヤを理解して、高レベルの割り当てが物理ページの使用量にどのように変換されるかを確認する必要があります。

メモリタイプ: 匿名とファイル バックアップ

OS がメモリを再利用する方法を詳しく説明する前に、Linux のメモリページの 2 つの基本的なカテゴリを理解する必要があります。

  1. 匿名メモリ(anon): ストレージ上のファイルに基づくことのないメモリ。これには、C / C++ malloc(Scudo アロケータなど)によるメモリ割り当てと、Java / Kotlin ヒープ上の Java オブジェクトに対するメモリ割り当てが含まれます。このメモリには戻り先のソースファイルがないため、OS は RAM に保持するか、メモリ不足時に圧縮して ZRAM にスワップアウトする必要があります。
  2. ファイルにバックアップされたメモリ(file): ストレージ上のファイルから直接マッピングされるメモリ。これには、実行可能コード(DEX ファイル、.so ネイティブ ライブラリ)とフォントが含まれます。

メモリマップ ファイル(mmap)

Android は mmap を使用して、ファイルベースのメモリと匿名メモリの両方をプロセスのアドレス空間にマッピングします。

ファイルにバックアップされたメモリを扱う場合、ページはさらに次のように分類されます。

  • クリーンメモリ: 変更されていないファイルからマッピングされたページ。システムでより多くのメモリが必要な場合、カーネルはこれらのページを単にドロップできます。これらのページは、後でストレージ上のファイルから直接再読み込みできるためです。
  • ダーティメモリ: プロセスによって変更されたページ。これらはドロップできません。匿名メモリのように動作し、RAM に保持するか、ZRAM に移動する必要があります。

Android では、メモリ マッピングに適したファイル形式(DEX など)が優先されるため、システムはメモリ不足時にクリーンメモリを簡単に再利用できます。匿名メモリとダーティメモリは単純にドロップできないため、プライベート ダーティメモリ / 匿名メモリが多いことが、システム スラッシングと Low Memory Killer(LMK)介入の主な原因となります。アプリが大量のプライベート ダーティメモリを使用している場合、パフォーマンスの急激な低下に直接つながります。

Zygote プロセスモデル

Android では、Zygote と呼ばれるプロセスを使用して、新しいアプリケーションの起動コストを最小限に抑えています。

  1. Zygote は起動時に起動し、共通のフレームワーク クラスとリソースをメモリにプリロードします。カーネルの観点からは、これは匿名ダーティメモリになりますが、Zygote プロセスに固有のものです。
  2. 新しいアプリが起動すると、システムは Zygote プロセスを fork します。
  3. 新しい子プロセスは、コピーオンライト(COW)セマンティクスを持つ共有マッピングを使用して、Zygote のメモリを継承します。

コピーオンライト セマンティクスを使用して、新しくフォークされた子プロセスとメモリページを共有する Zygote プロセスを示す図

子プロセスが Zygote から継承されたメモリのみを読み取る限り、物理メモリページはすべてのプロセス間で共有されたままになります。子プロセスが共有ページを変更すると、カーネルはそのプロセスのプライベート コピーを透過的に作成します。このモデルにより、多くのプロセスがメモリの大部分(特にフレームワークのコードとリソース)を共有できるため、システム全体のメモリ使用量が大幅に削減されます。

RSS、PSS、USS、memcg

メモリは、主に Zygote モデルを介してプロセス間で頻繁に共有されるため、プロセスのメモリ使用量を考慮する方法はいくつかあります。RSS、PSS、USS は、プロセスの仮想アドレス空間にマッピングされているページをカウントします。

Resident Set Size(RSS)、Proportional Set Size(PSS)、Unique Set Size(USS)の違いを視覚化した図。共有メモリページがどのようにカウントされるかを示しています。

  • RSS(Resident Set Size): プロセスが RAM に保持しているページの合計数。共有ページが複数回(共有しているプロセスごとに 1 回)カウントされるため、使用量が過大に評価されます。
  • PSS(Proportional Set Size): プロセスに固有のメモリの合計量に、共有メモリの比例配分を加えたものです。ページが 5 つのプロセスで共有されている場合、各プロセスはそのページの 5 分の 1 を占有しているとみなされます。
  • USS(Unique Set Size): プロセスに固有のメモリ量。これは、プロセスが強制終了された場合にシステムに返されるメモリです。

memcg: カーネル独自のアカウンティング

カーネルは、各プロセスが使用するメモリの合計を、そのメモリがマッピングされているかどうかにかかわらず、独自に保持します。Android では、すべてのアプリプロセスが独自のメモリ制御グループ(memcg)に属し、アプリ全体の memcg 内にネストされています(Android memcg 階層を参照)。メモリのページが割り当てられるたびに、カーネルはそれを割り当てたプロセスの memcg に課金します。合計料金は、memcg の memory.current ファイルにあります。

memcg のアカウンティングは、RSS、PSS、USS とはいくつかの重要な点で異なります。

  • 各ページは、最初にメモリに読み込んだプロセスの memcg に対して 1 回だけ課金されます。他のプロセスが後で同じページをマッピングしても、このチャージはそのまま残ります。Zygote がプリロードするページは、アプリではなく Zygote に課金されます。アプリがこれらのページのいずれかに書き込むと、プライベート コピーがアプリに課金されます。
  • これには、ページテーブル指標で確認できないメモリが含まれます。memory.current は、アプリがマッピングせずに読み取るファイルページ(read または pread で読み取られるデータベース ページなど)をカウントします。
  • スワップは個別に追跡されます。ZRAM に圧縮されたメモリは memory.current から memory.swap.current に移動するため、推測することなく両方を確認できます。
  • 読み取りコストが低い。カウンタは常に最新の状態に保たれ、カウンタを読み取ってもアプリのアドレス空間がロックされたりスキャンされたりすることはありません。
  • これはシステムの上限です。Android がアプリに適用するメモリ上限とメモリ予算は、memcg チャージに作用します。

ハードウェア ビットマップの背後にあるバッファなどのグラフィック メモリは、通常、アプリの memcg には課金されません。

どの指標をいつ使用するか

適切なメモリ指標の選択は、測定対象と測定環境によって異なります。Android 17 QPR2(API レベル 37.2)以降を搭載したデバイスで、独自のアプリを追跡する場合は、まず memcg を使用します。

指標 最適な用途 主な強み 主なデメリット
memcg 個々のアプリを時系列で追跡し、予算と比較する 各ページを 1 回カウントし、マッピングされていないページ キャッシュを含め、スワップを個別に追跡します。読み取りが安価。 Android 17 QPR2 より前のデバイスでは、一部のデバイスで利用できません。共有ページは、最初にアクセスしたプロセスに課金されます。
PSS システム全体のメモリ スナップショット 共有メモリを正確に割り当てます。PSS の合計は、使用されたメモリの合計に等しくなります。 実行中の他のプロセスに応じて変動します。特定のアプリを時系列で比較するのには適していません。
RSS 個々のアプリの経時的な追跡、フィールド テレメトリ 安定しており、他のプロセスから独立している。収集が迅速かつ安価。 共有メモリを複数回カウントするため、合計使用量が過大評価されます。
USS プロセスを強制終了した場合の影響を評価する アプリが終了した場合に解放されるメモリの量を正確に示します。 共有メモリを完全に無視します。

memcg(メモリ制御グループのチャージ)

  • 最適な用途: Android 17 QPR2 以降を搭載したデバイスで、個々のアプリのメモリ使用量を、時間の経過、クリティカル ユーザー ジャーニー(CUJ)、アプリのバージョン間で追跡する場合。また、メモリ予算を調整する際に注目すべき指標でもあります。これは、予算が制限する数であるためです。
  • 理由: RSS と同様に、他のアプリがアプリとのページ共有を開始または停止しても、memcg チャージは変更されません。RSS とは異なり、ページが 2 回カウントされることはないため、アプリの実行中のすべてのプロセスのチャージを合計できます。アプリ全体の memcg は、カーネルが再利用するまで終了したプロセスのキャッシュされたファイルページを保持するため、その合計よりも大きくなる可能性があります。また、マッピングされていないファイル キャッシュなど、RSS、PSS、USS が見逃すメモリもカウントします。カーネルはページを課金するときにカウンタを更新するため、読み取りは低コストです。
  • 読み取り方法: プロセスの memcg ディレクトリにある memory.current、memory.swap.current、memory.stat を読み取ります(memcg 統計情報をご覧ください)。Android 18 以降では、dumpsys meminfo もこれらを出力します。アプリ内から MemoryBudgetManager を使用します。
  • フィールド測定のベスト プラクティス: RSS で説明したのと同じ理由で、匿名メモリ + スワップ(memory.stat の anon フィールドと memory.swap.current)を追跡します。memory.stat の file フィールドには、アプリが一度読み取ったファイルページが含まれており、カーネルはまだ再利用していないため、アプリがそのメモリの使用を停止した後もしばらくの間、memory.current は高いままになることがあります。
  • 注意が必要な場合: ページは、最初にアクセスしたプロセスに課金されるため、アプリが他のプロセスよりも先に読み取った共有ファイル ページ(フォントや共有ライブラリなど)は、アプリに課金されます。逆に、アプリが使用するページが別のプロセスに課金されることもあります。ファイルにバックアップされたメモリについては、デバイスや実行ごとにわずかな違いが生じることがあります。比較する際は、anon とスワップを参考にしてください。

PSS(Proportional Set Size)

  • 用途: 特定のデバイスのシステムメモリのスナップショットを特定の時点で完全に取得する。
  • 理由: PSS には、実行中のすべてのプロセスの PSS の合計が、システム内のプロセスで使用されているメモリの合計と等しくなるという、有用な数学的特性があります。共有メモリを重複カウントすることなく、完全に割り当てます。
  • 使用を避けるべき場合: 2 つの異なるスナップショット、異なる時間、異なるデバイス間で特定のプロセスを比較するために PSS を使用しないでください。PSS は、現在プロセスとページを共有している他のプロセスの数に依存するため、アプリの実際の動作がまったく変わらなくても、アプリの PSS が変動する可能性があります。

RSS(Resident Set Size)

  • 最適な用途: 個々のアプリのメモリ使用量を、さまざまなクリティカル ユーザー ジャーニー(CUJ)にわたって、またはアプリのバージョン間で比較して、時系列で追跡する。また、フィールド テレメトリーに最も実用的な選択肢でもあります。
  • 理由: RSS は PSS よりも安定しています。他のアプリの動作に関係なく、プロセスにマッピングされたメモリを示します。さらに、RSS は低コストで収集できます。PSS または USS を測定するには、複雑なカーネルメモリ管理構造をスキャンしてロックを取得する必要があります。収集の頻度が高すぎると、システム パフォーマンスの問題(UI のジャンクなど)が発生する可能性があります。
  • フィールド測定のベスト プラクティス: 実際のデバイスからデータを収集する場合は、通常、匿名 RSS + スワップ(ZRAM)を測定するのが最適です。
    • 匿名のみの理由ファイルにバックアップされたページ(コード、アプリリソース、フォント、その他のメモリマップド ファイルなど)は、システム全体のページキャッシュの負荷により、カーネルによっていつでも強制排除される可能性があります。特定のアプリを追跡する指標にファイルページを含めると、OS のメモリ管理の決定によるノイズが発生します。これは、他のアプリやシステムからのメモリ負荷によって引き起こされる可能性があります。逆に、匿名メモリ(Java ヒープやネイティブ ヒープなど)は、アプリによって直接制御されます。
    • スワップを追加する理由メモリが不足すると、OS は匿名ページを圧縮して ZRAM(スワップ)に移動します。常駐の匿名メモリのみを測定すると、システムがメモリ不足に陥ってページをスワップアウトしただけで、メモリ使用量が「減少」したと指標に誤って表示されることがあります。スワップされたメモリを追加すると、アプリが割り当てたすべての匿名メモリ(現在 RAM または ZRAM に存在するかどうかを問わない)を考慮できます。
    • 可能な場合は memcg を優先します。Android 17 QPR2 以降を搭載したデバイスでは、memory.stat の anon と memory.swap.current を組み合わせることで、Zygote と共有されている匿名ページを除外した同様の測定値が得られます。また、収集にかかるコストも低くなります(memcg を参照)。

USS(Unique Set Size)

  • 最適な用途: プロセスを強制終了した場合のシステムへの即時的な影響を判断する。
  • 理由: USS は、プロセスが終了した場合(たとえば、ローメモリ キラーデーモン lmkd によって終了した場合)に、すぐに回収されてシステムに返される正確なメモリ量を表します。
  • 使用方法: この指標は、永続的なプロセスやバックグラウンド サービスを評価する際に非常に役立ちます。バックグラウンド プロセスのシステム全体のコストを評価する場合、USS は、その特定のプロセスを維持するためにシステムが排他的に犠牲にしているメモリの量を正確に示します。

↑ 上へ | ツール →