メモリ管理の概要

Android Runtime(ART)と Dalvik 仮想マシンは、ページングメモリ マッピングを使用してメモリを管理します。つまり、アプリが変更するメモリは、新しいオブジェクトの割り当てか、マップされたページへのアクセスかにかかわらず、RAM に常駐したままで、ページアウトできません。アプリからメモリを解放する唯一の方法は、アプリが保持するオブジェクト参照を解放し、そのメモリをガベージ コレクタの処理対象とすることです。ただし、例外が 1 つあります。コードなど、変更なしでメモリマップされたファイルは、システムがそのメモリを他の場所で使用する場合に、RAM からページアウトできます。

このページでは、Android がアプリのプロセスとメモリ割り当てを管理する方法について説明します。アプリでメモリをより効率的に管理する方法の詳細については、アプリのメモリを管理するをご覧ください。

ガベージ コレクション

ART や Dalvik 仮想マシンなどのマネージド メモリ環境では、各メモリ割り当てが追跡されます。プログラムが使用しなくなったと判断されたメモリは、プログラマが関与することなく、ヒープに戻されます。マネージド メモリ環境で未使用のメモリを回収するメカニズムは、ガベージ コレクションと呼ばれています。ガベージ コレクションには 2 つの目的があります。プログラム内のアクセスできなくなったデータ オブジェクトを見つけることと、それらのオブジェクトが使用しているリソースを回収することです。

Android のメモリヒープは世代別ヒープになっています。これは、割り当てられるオブジェクトの予想寿命とサイズによって、追跡する割り当てのバケットが異なることを意味しています。たとえば、最近割り当てられたオブジェクトは、新しい世代に属します。オブジェクトが十分な期間にわたってアクティブであると、古い世代に昇格され、その後は永続の世代に昇格されます。

各世代のヒープには、そこでオブジェクトが占有できるメモリ量に対する上限があります。世代が満杯になると、システムがガベージ コレクション イベントを実行してメモリを解放します。ガベージ コレクションが保持される期間は、回収するオブジェクトの世代と、各世代にあるアクティブなオブジェクトの数によって異なります。

ガベージ コレクションは非常に高速ですが、それでもアプリのパフォーマンスに影響を与える可能性があります。通常、コード内からガベージ コレクション イベントが発生するタイミングを制御することはありません。システムには、ガベージ コレクションを実行するタイミングを決定するための一連の実行条件があります。条件が満たされると、システムはプロセスの実行を停止し、ガベージ コレクションを開始します。ガベージ コレクションが、アニメーションのような集中的な処理ループの途中や音楽の再生中に発生すると、処理時間が長くなる可能性があります。これによって、アプリのコード実行が、効率的で滑らかなフレーム レンダリングのために推奨される 16 ミリ秒のしきい値を超えてしまう可能性があります。

また、コードフローによっては、ガベージ コレクションのイベントを頻繁に発生させる処理や、通常よりも長くする処理などを実行することがあります。たとえば、アルファ ブレンディング アニメーションの各フレーム中に for ループの最も内側で複数のオブジェクトを割り当てると、メモリヒープを多数のオブジェクトで汚染する可能性があります。このような状況では、ガベージ コレクタが複数のガベージ コレクション イベントを実行して、アプリのパフォーマンスを低下させる可能性があります。

ガベージ コレクションの概要については、ガベージ コレクションをご覧ください。

メモリを共有する

Android は、必要なものをすべて RAM に収めるために、RAM ページをプロセス間で共有しようとします。これには、次の方法があります。

  • 各アプリケーション プロセスは Zygote という既存のプロセスから fork されます。 Zygote プロセスは、システム起動時に開始され、共通のフレームワークのコードとリソース(アクティビティ テーマなど)を読み込みます。新しいプロセスを開始するときは、システムが Zygote プロセスを fork してから、新しいプロセスでアプリのコードをロードして実行します。この方法により、フレームワークのコードとリソースに割り当てられた RAM ページのほとんどを、すべてのアプリプロセスで共有できます。
  • ほとんどの静的データは、プロセスにメモリマップされます。この方法により、プロセス間でデータを共有することができ、必要に応じてページアウトすることもできます。こういった静的データには、Dalvik コード(直接メモリマップするため事前にリンクされた .odex ファイルに入れる)、アプリリソース(リソース テーブルをメモリマップできる構造に設計し、APK の zip エントリを境界に合わせる)、.so ファイルのネイティブ コードといった従来型のプロジェクトの構成要素などがあります。
  • Android では、(ashmem と gralloc のいずれかを使用して)明示的に割り当てられた共有メモリ領域を使用して、プロセス間で同じ動的 RAM を共有します。たとえば、ウィンドウ サーフェスはアプリと画面コンポジタの間で共有メモリを使用し、カーソル バッファはコンテンツ プロバイダとクライアントの間で共有メモリを使用します。

共有メモリが広範囲に使用されるため、アプリが使用しているメモリ量を判断するには注意が必要です。アプリのメモリ使用量を適切に判断する方法については、RAM 使用量の調査をご覧ください。

アプリメモリの割り当てと回収

Dalvik ヒープは、アプリプロセスごとに 1 つの仮想メモリ範囲に制限されます。これにより論理ヒープサイズが決まります。論理ヒープサイズは、必要に応じて増やすことができますが、システムが各アプリに決めた上限を超えることはできません。

ヒープの論理サイズは、ヒープで使用される物理メモリの量とは異なります。Android は、アプリのヒープを調査する際に Proportional Set Size(PSS)と呼ばれる値を計算します。このとき、他のプロセスと共有されるダーティページとクリーンページの両方を計算に入れますが、対象となるのはその RAM を共有するアプリの数に比例した量だけです。この(PSS の)合計は、システムが物理メモリ フットプリントと見なすものです。PSS の詳細については、RAM 使用量の調査のガイドをご覧ください。

Dalvik ヒープは、ヒープの論理サイズを圧縮しません。つまり、Android がヒープをデフラグして空きスペースを詰めることはしません。Android は、ヒープの最後に未使用のスペースがある場合にのみ論理ヒープサイズを縮小できます。ただし、システムはヒープが使用する物理メモリは減らすことができます。ガベージ コレクションの後、Dalvik はヒープを調べて未使用のページを見つけ、そのページを madvise を使用してカーネルに返します。そのため、大きなチャンクの割り当てと解放を対にして行うと、使用されているすべての(またはほぼすべての)物理メモリが解放されるはずです。ただし、小さな割り当てを解放する場合は、はるかに非効率になる可能性があります。これは、小さな割り当てに使用されたページが、まだ解放されていない他のページと共有されている可能性があるためです。

アプリメモリを制限する

Android では、マルチタスク環境の機能を維持するために、アプリごとのヒープサイズにハードリミットが設定されています。正確なヒープサイズの上限は、デバイス全体で使用可能な RAM の量に基づき、デバイスごとに異なります。アプリがこのヒープ容量の上限に達し、さらにメモリを割り当てようとすると、アプリで OutOfMemoryError が発生する可能性があります。

場合によっては、システムに問い合わせて、現在のデバイスで使用できるヒープ領域の正確な量を確認することもできます(たとえば、安全にキャッシュに保持できるデータ量を確認する)。この量をシステムに問い合わせるには getMemoryClass() を呼び出します。このメソッドは、アプリのヒープで使用できるメガバイト数を示す整数を返します。

アプリの切り替え

ユーザーがアプリを切り替えると、Android はフォアグラウンドではない(つまり、ユーザーに表示されない、または音楽再生などのフォアグラウンド サービスを実行していない)アプリをキャッシュに保持します。たとえば、ユーザーが最初にアプリを起動すると、そのためのプロセスが作成されます。しかし、ユーザーがアプリを離れても、そのプロセスは終了しません。 システムはプロセスをキャッシュに保持します。ユーザーが後でアプリに戻ると、システムはプロセスを再利用するため、アプリの切り替えが速くなります。

アプリにキャッシュされたプロセスがあり、それが現在必要のないリソースを保持している場合、アプリは(ユーザーが使用していないときでも)システム全体のパフォーマンスに影響を与えます。メモリなどのリソースが少なくなると、システムはキャッシュ内のプロセスを強制終了します。また、システムは、ほとんどのメモリを保持しているプロセスも考慮し、それらを終了して RAM を解放する可能性もあります。

注: キャッシュ内でアプリが消費するメモリが少ないほど、強制終了されずにすぐに再開できる可能性が高くなります。ただし、瞬間的なシステム要件によって、リソース使用率に関係なく、キャッシュされたプロセスを随時、強制終了させることができます。

フォアグラウンドで実行されていないプロセスをキャッシュし、終了可能なプロセスを Android が決定する仕組みについて詳しくは、プロセスとスレッドのガイドをご覧ください。

メモリ負荷テスト

メモリ負荷の問題は、ハイエンドのデバイスではあまり見られませんが、Android(Go バージョン)搭載デバイスなど、RAM の少ないデバイスでは現在も発生することがあります。インストルメンテーション テストを作成してアプリの動作を検証することで、メモリの少ないデバイスでのユーザー エクスペリエンスを改善できるよう、メモリ負荷の高い環境を試して再現することが重要です。

Stressful Application Test

Stressful Application Teststressapptest)は、実際に負荷の高い状況を作ってアプリのさまざまなメモリ制限やハードウェア制限をテストできるメモリ インターフェース テストです。時間制限とメモリ制限を定義する機能を使って、計測手法を作成し、メモリ負荷の高い状況の実際の発生を確認できます。 たとえば、次のコマンドセットでは、データ ファイル システムで静的ライブラリにプッシュ操作を行って実行可能にし、20 秒間 990 MB の負荷テストを行うことができます。
    adb push stressapptest /data/local/tmp/
    adb shell chmod 777 /data/local/tmp/stressapptest
    adb shell /data/local/tmp/stressapptest -s 20 -M 990

  

ツールのインストール、一般的な引数、エラー処理の情報について詳しくは、stressapptest のドキュメントをご覧ください。

負荷テストの結果

stressapptest などのツールを使って、空き容量より多いメモリ割り当てをリクエストできます。このタイプのリクエストを行うと、さまざまなアラートが発生する可能性があるため、開発サイドから注意する必要があります。空きメモリ不足が原因で発生する可能性がある主なアラートは次の 3 つです。
  • SIGABRT: これは、システムにすでにメモリ負荷がかかっているのに、空き容量より大きいサイズの割り当てをリクエストすることによって生じる、致命的な、ネイティブ コードでのクラッシュです。
  • SIGQUIT: コアメモリ ダンプを生成し、インストルメンテーション テストで検知されるとプロセスを終了します。
  • TRIM_MEMORY_EVENTS: このコールバックは Android 4.1(API レベル 16)以降で利用でき、プロセスの詳細なメモリアラートを提供します。