メモリ管理の概要

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

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

注: システムが LRU キャッシュ内のプロセスの終了を開始するときは、基本的に最下位から順に処理します。また、どのプロセスがより多くのメモリを消費していて、終了するとより多くのメモリが得られることも考慮します。消費しているメモリが LRU リスト全体の中で少ないほど、リストに残って、すぐに再開できる可能性が高くなります。

フォアグラウンドで実行されていないプロセスがどのようにキャッシュされ、終了させるプロセスを Android がどのように決めるかについての詳細は、プロセスとスレッドのガイドをご覧ください。