Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

Memory Profiler を使用して Java ヒープとメモリ割り当てを表示する

Memory Profiler は Android Profiler のコンポーネントであり、ぎこちない動き、フリーズ、さらにはアプリのクラッシュを引き起こす可能性があるメモリリークやメモリチャーンを特定するのに役立ちます。 このコンポーネントでは、アプリのメモリ使用量のリアルタイム グラフが表示され、ヒープダンプの取得、ガベージ コレクションの強制実行、メモリ割り当ての追跡が可能です。

Memory Profiler を開くには、次の手順を実行します。

  1. [View] > [Tool Windows] > [Android Profiler] をクリックします(ツールバーの [Android Profiler] をクリックすることもできます)。
  2. Android Profiler のツールバーからプロファイリングする端末とアプリプロセスを選択します。 USB 経由で端末に接続しているが、端末がリストに表示されない場合は、USB デバッグを有効にしていることを確認します。
  3. [MEMORY] タイムラインの任意の場所をクリックして Memory Profiler を開きます。

または、コマンドラインで dumpsys を使ってアプリのメモリを調べたり、logcat の GC イベントを表示したりできます。

アプリのメモリをプロファイリングする必要がある理由

Android は管理メモリ環境を備えています。アプリが一部のオブジェクトを使用していないことが検出されると、ガベージ コレクターは未使用のメモリをリリースしてヒープに戻します。 Android が未使用のメモリを検出する仕組みは継続的に改善されていますが、すべての Android バージョンにおいて、ある時点でコードの実行を短時間一時停止する必要があります。 ほとんどの場合、その一時停止は知覚できないものです。 ただし、アプリによって、システムがメモリを収集できる以上の速度でメモリが割り当てられると、コレクターが十分なメモリを解放して割り当てを満たしているにもかかわらず、アプリで遅延が発生する場合があります。 この遅延により、アプリがフレームをスキップしたり、明らかに速度が低下する場合があります。

アプリで速度低下が発生していない場合でも、メモリリークが発生していれば、アプリはバックグラウンドにあるときでもそのメモリを保持できます。 この動作により、不必要なガベージ コレクション イベントが強制実行され、システム上の残りのメモリ パフォーマンスが低下する可能性があります。 最終的には、システムによりアプリ プロセスが強制終了され、メモリが回収されます。 その後、ユーザーがアプリに戻ったとき、アプリは完全に再起動する必要があります。

こうした問題を防ぐには、Memory Profiler を使用して、次の措置を講じる必要があります。

  • パフォーマンス上の問題を引き起こす可能性がある、望ましくないメモリ割り当てパターンをタイムラインで探します。
  • Java ヒープをダンプして、任意の時点でメモリを使い切っているオブジェクトを特定します。 長期間にわたるいくつかのヒープダンプがメモリリークの特定に役立つ可能性があります。
  • ユーザーによる通常の操作および極端な操作の際のメモリ割り当てを記録して、コードが短時間にオブジェクトを割り当てすぎている領域、またはメモリリークが発生するオブジェクトを割り当てている領域を正確に特定します。

アプリのメモリ使用量を削減できるプログラミング上のベスト プラクティスについては、アプリのメモリの管理をご覧ください。

Memory Profiler の概要

Memory Profiler を最初に開くと、アプリのメモリ使用量の詳細なタイムラインと、ガベージ コレクションの強制実行、ヒープダンプの取得、メモリ割り当ての記録を行えるアクセスツールが表示されます。

図 1. Memory Profiler

図 1 に示すように、Memory Profiler のデフォルト ビューには次の項目が含まれます。

  1. ガベージ コレクション イベントを強制実行するボタン。
  2. ヒープダンプを取得するボタン。
  3. メモリ割り当てを記録するボタン。 このボタンは、Android 7.1 以前を実行している端末を接続した場合にのみ表示されます。
  4. タイムラインを拡大 / 縮小するボタン。
  5. ライブメモリ データにジャンプするボタン。
  6. activity の状態、ユーザー入力イベント、画面の回転イベントを示すイベント タイムライン。
  7. 次の要素が含まれるメモリ使用量タイムライン。
    • 各メモリカテゴリによって使用されているメモリ使用量の積み重ねグラフ。メモリ使用量は左の Y 軸で、メモリカテゴリは上部の色キーで示されています。
    • 破線は割り当てられたオブジェクトの数を表しています。オブジェクトの数は右の Y 軸で示されています。
    • 各ガベージ コレクション イベントのアイコン。

ただし、Android 7.1 以前を実行している端末を使用している場合は、すべてのプロファイリング データがデフォルトで表示されるわけではありません。 「Advanced profiling is unavailable for the selected process」というメッセージが表示された場合は、次の要素を表示するために、詳細なプロファイリングを有効にする必要があります。

  • イベント タイムライン
  • 割り当てられたオブジェクトの数
  • ガベージ コレクション イベント

Android 8.0 以降では、デバッグ可能なアプリに対して詳細なプロファイリングが常に有効になります。

メモリをカウントする方法

Memory Profiler の上部に表示される数値(図 2)は、Android システムに応じて、アプリがコミットしたすべてのプライベート メモリ ページに基づいています。 このカウントには、システムまたは他のアプリが共有するページは含まれません。

図 2. Memory Profiler の上部にあるメモリカウントの凡例

メモリカウントのカテゴリは次のとおりです。

  • Java: Java または Kotlin コードから割り当てられたオブジェクトのメモリ。
  • Native: C または C++ コードから割り当てられたオブジェクトのメモリ。

    Android フレームワークでは、コードを Java または Kotlin で記述していても、画像アセットやその他のグラフィックを処理するときなど、ネイティブ メモリを使用してユーザーのさまざまなタスクが処理されるため、アプリで C++ を使用していない場合でも、使用された一部のネイティブ メモリがここに表示される場合があります。

  • Graphics: GL サーフェスや GL テクスチャなど、画面にピクセルを表示するためにグラフィック バッファキューに使用されたメモリ (このメモリは、専用の GPU メモリではなく、CPU と共有されるメモリであることに注意してください)。

  • Stack: アプリのネイティブ スタックと Java スタックの両方によって使用されたメモリ。 通常、このメモリはアプリが実行しているスレッドの数に関係します。

  • Code: dex バイトコード、最適化またはコンパイルされた dex コード、.so ライブラリ、フォントなど、コードやリソースのためにアプリが使用したメモリ。

  • Other: アプリによって使用された、カテゴリが不明なメモリ。

  • Allocated: アプリによって割り当てられた Java および Kotlin オブジェクトの数。 C または C++ で割り当てられたオブジェクトはカウントされません。

    Android 7.1 以前を実行している端末を接続した場合、この割り当てカウントは、Memory Profiler が実行中のアプリに接続された時点で初めて開始されます。 したがって、プロファイリングを開始する前に割り当てられたオブジェクトはカウントされません。 ただし、Android 8.0 はすべての割り当てを追跡する端末プロファイリング ツールを備えているため、この数値は、Android 8.0 以降で実行されているアプリの応答しない Java オブジェクトの合計数を常に表します。

以前の Android Monitor ツールのメモリカウントと比較すると、新しい Memory Profiler はメモリを別の方法で記録するため、メモリ使用量が多くなったように見える場合があります。 Memory Profiler は、合計数を増加させることになるいくつかの追加カテゴリを監視しますが、Java ヒープメモリのみに関しては、「Java」の数値は以前のツールの値と同じようになるはずです。

また、おそらく Java の数値は Android Monitor での値と正確に一致しませんが、アプリの Java ヒープは Zygote からフォークされたため、この新しい数値には、このヒープに割り当てられたすべての物理メモリページがカウントされています。 したがって、この数値は、アプリが実際に使用している物理メモリの量を正確に表しています。

注: 現在、Memory Profiler は、アプリの誤検知されたネイティブ メモリ使用量(実際にはプロファイリング ツールによるメモリ使用量)も表示します。 最大 10 万個のオブジェクトに最大 10MB のメモリが追加されます。 このツールの将来のバージョンでは、これらの数値はデータから除外されます。

メモリ割り当てを表示する

メモリ割り当ては、メモリの各オブジェクトがどのように割り当てられたかを示します。 特に、Memory Profiler はオブジェクト割り当てに関して次の内容を表示できます。

  • 割り当てられたオブジェクトのタイプとオブジェクトのメモリ使用量。
  • 各割り当てのスタック トレースと含まれているスレッド。
  • オブジェクトが割り当て解除されたタイミング(Android 8.0 以降を実行している端末を使用している場合のみ)

端末が Android 8.0 以降を実行している場合は、次のようにして、いつでもオブジェクト割り当てを表示できます。 タイムラインをクリックしたままドラッグして、割り当てを表示する領域を選択します(動画 1 を参照)。 Android 8.0 以降には、アプリの割り当てを常に追跡する端末プロファイリング ツールが含まれているため、記録セッションを開始する必要はありません。

動画 1. Android 8.0 以降で、タイムラインの既存の領域を選択してオブジェクト割り当てを表示します。

端末が Android 7.1 以前を実行している場合は、Memory Profiler のツールバーで [Record memory allocations] をクリックします。 Android Monitor は記録中にアプリで発生するすべての割り当てを追跡します。 記録が完了したら、[Stop recording] (同じボタン。動画 2 を参照)をクリックして、割り当てを表示します。

動画 2. Android 7.1 以前では、メモリ割り当てを明示的に記録する必要があります。

タイムラインの領域を選択したら(または、Android 7.1 以前を実行している端末を使用して記録セッションを完了したら)、クラス名でグループ化され、ヒープカウントの順に並べられた割り当て済みのオブジェクトがタイムラインの下に表示されます。

注: Android 7.1 以前では、最大 65535 個の割り当てを記録できます。 記録セッションがこの制限を超えると、最新の 65535 個の割り当てのみが記録に保存されます (Android 8.0 以降では、事実上の制限はありません)。

割り当て記録を検査するには、次の手順を実行します。

  1. リストに目をとおして、ヒープカウントが著しく大きい、リークが発生している可能性があるオブジェクトを見つけます。 既知のクラスを見つけやすくするには、[Class Name] 列の見出しをクリックして、アルファベット順に並べ替えます。 その後、クラス名をクリックします。 図 3 のように、右側に [Instance View] ペインが開き、そのクラスの各インスタンスが表示されます。
  2. [Instance View] ペインで、インスタンスをクリックします。 ペインの下に [Call Stack] タブが表示され、そのインスタンスが割り当てられた場所と含まれるスレッドが示されます。
  3. [Call Stack] タブで任意の行をクリックして、エディタにそのコードを表示します。

図 3. 割り当てられた各オブジェクトの詳細は、右側の [Instance View] ペインに表示されます。

デフォルトでは、左側の割り当てリストはクラス名別に並べられます。 リスト上部の右側にあるプルダウンを使用して、次の並べ方に切り替えることができます。

  • Arrange by class: クラス名に基づいてすべての割り当てをグループ化します。
  • Arrange by package: パッケージ名に基づいてすべての割り当てをグループ化します。
  • Arrange by callstack: すべての割り当てを対応するコールスタックにグループ化します。

ヒープダンプを取得する

ヒープダンプは、ヒープダンプの取得時にアプリのどのオブジェクトがメモリを使用しているかを示します。 特に、長いユーザー セッションの後、ヒープダンプにより、メモリに存在しないと思われるオブジェクトが引き続きメモリにあることが示されるため、メモリリークの特定が容易になります。 ヒープダンプを取得した後、次の内容を表示できます。

  • アプリが割り当てたオブジェクトのタイプと各タイプのオブジェクト数。
  • 各オブジェクトが使用しているメモリ量。
  • コードで各オブジェクトへの参照が保持される場所。
  • オブジェクトが割り当てられた場所のコールスタック (現在、割り当ての記録中にヒープダンプを取得した場合、Android 7.1 以前を使用して取得されたヒープダンプのみでコールスタックは利用できます)。

図 4. ヒープダンプの表示

ヒープダンプを取得するには、Memory Profiler のツールバーで [Dump Java heap] をクリックします。 ヒープをダンプする際に、Java メモリの使用量が一時的に増加する場合があります。 これは通常の現象です。ヒープダンプはアプリと同じプロセスで発生し、データを収集するためにいくらかのメモリを要求するからです。

図 4 のように、ヒープダンプはメモリ タイムラインの下に表示され、ヒープのすべてのクラスタイプを示します。

注: より厳密なタイミングでダンプを作成したい場合は、アプリコード内の重要な箇所で dumpHprofData() を呼び出してヒープダンプを作成することができます。

ヒープを検査するには、次の手順を実行します。

  1. リストに目をとおして、ヒープカウントが著しく大きい、リークが発生している可能性があるオブジェクトを見つけます。 既知のクラスを見つけやすくするには、[Class Name] 列の見出しをクリックして、アルファベット順に並べ替えます。 その後、クラス名をクリックします。 図 5 のように、右側に [Instance View] ペインが開き、そのクラスの各インスタンスが表示されます。
  2. [Instance View] ペインで、インスタンスをクリックします。 ペインの下に [References] タブが表示され、そのオブジェクトへのすべての参照が示されます。

または、インスタンス名の横にある矢印をクリックして、そのすべてのフィールドを表示してから、フィールド名をクリックして、そのすべての参照を表示します。 フィールドのインスタンスの詳細を表示する場合は、フィールドを右クリックして、[Go to Instance] を選択します。

  1. [References] タブで、メモリリークが発生している可能性のある参照を特定するには、タブを右クリックして、[Go to Instance] を選択します。 これにより、ヒープダンプから対応するインスタンスが選択され、そのインスタンス データが表示されます。

ヒープダンプでは、割り当てられた各オブジェクトのスタック トレースはデフォルトで表示されません。 スタック トレースを取得するには、[Dump Java heap] をクリックする前に、メモリ割り当ての記録を開始する必要があります。 その後、図 5 のように、[Instance View] でインスタンスを選択し、[References] タブの横に [Call Stack] タブを表示することができます。ただし、割り当ての記録を開始する前に、いくつかのオブジェクトが割り当てられている可能性があり、これらのオブジェクトのコールスタックは利用できません。 コールスタックを含まないインスタンスは、アイコン上の「スタック」バッジ で示されます (残念ながら、スタック トレースでは、ユーザーが割り当ての記録を実行する必要があるため、Android 8.0 を使用している場合は、現在、ヒープダンプのスタック トレースを表示できません)。

ヒープダンプでは、次のいずれかが原因で生じたメモリリークに注目します。

  • ActivityContextViewDrawable、および Activity または Context コンテナへの参照を保持しているその他のオブジェクトへの長命参照。
  • Activity インスタンスを保持できる、Runnable などの静的ではない内部クラス。
  • 必要以上に長くオブジェクトを保持するキャッシュ。

図 5. ヒープダンプの取得に必要な時間がタイムラインに示されます。

クラスのリストには、次の情報が表示されます。

  • Heap Count: ヒープ内のインスタンス数。
  • Shallow Size: このヒープ内のすべてのインスタンスの合計サイズ(バイト単位)。
  • Retained Size: このクラスのすべてのインスタンスのために保持されているメモリの合計サイズ(バイト単位)。

クラスリストの上部で左のプルダウン リストを使用して、次のヒープダンプに切り替えることができます。

  • Default heap: システムによりヒープが指定されていない場合。
  • App heap: アプリがメモリを割り当てるプライマリ ヒープ。
  • Image heap: 起動時にプリロードされたクラスを含むシステム ブートイメージ。 この割り当てが変化することはありません。
  • Zygote heap: Android システムでアプリプロセスがフォークされたコピー オン ライト ヒープ。

デフォルトでは、ヒープのオブジェクト リストはクラス名別に並べられます。 もう 1 つのプルダウン リストを使用して、次の並べ方に切り替えることができます。

  • Arrange by class: クラス名に基づいてすべての割り当てをグループ化します。
  • Arrange by package: パッケージ名に基づいてすべての割り当てをグループ化します。
  • Arrange by callstack: すべての割り当てを対応するコールスタックにグループ化します。 このオプションは、割り当ての記録中にヒープダンプを取得する場合にのみ機能します。 ただし、記録の開始前にヒープにオブジェクトが存在する可能性があるため、これらの割り当ては最初にクラス名ごとに表示されます。

デフォルトでは、リストは [Retained Size] 列で並べ替えられます。 いずれかの列の見出しをクリックして、リストを並べ替える方法を変更できます。

[Instance View] ペインでは、各インスタンスの次の情報が表示されます。

  • Depth: GC ルートから選択済みのインスタンスへの最小ホップ数。
  • Shallow Size: このインスタンスのサイズ。
  • Retained Size: このインスタンスが(ドミネーター ツリーで)支配するメモリのサイズ

ヒープダンプを HPROF で保存する

ヒープダンプを取得した後は、Memory Profiler の実行中にのみデータが表示されます。 プロファイリング セッションを終了すると、ヒープダンプは失われます。 したがって、後でレビューするためにヒープダンプを保存する場合は、タイムラインの下にあるツールバーで [Export heap dump as HPROF file] をクリックして、ヒープダンプを HPROF ファイルにエクスポートします。 表示されたダイアログで、ファイルに .hprof 拡張子を必ず付けて保存します。

その後、そのファイルを空のエディタ ウィンドウにドラッグすると(または、ファイルタブ バーにドロップすると)、Android Studio でファイルを再度開くことができます。

jhat など、別の HPROF アナライザーを使用するには、HPROF ファイルを Android 形式から Java SE HPROF 形式に変換する必要があります。 android_sdk/platform-tools/ ディレクトリにある hprof-conv ツールを使用して、この変換を行えます。 元の HPROF ファイルと変換済みの HPROF ファイルの書き出し先の 2 つの引数を指定して、hprof-conv コマンドを 実行します。 次に例を示します。

hprof-conv heap-original.hprof heap-converted.hprof

メモリをプロファイリングするためのテクニック

Memory Profiler を使用しているとき、アプリコードに過度な負荷をかけて、メモリリークを強制的に発生させる必要があります。 アプリでメモリリークを発生させるには、ヒープを調査する前にしばらくの間アプリを実行するという方法があります。 リークは、ヒープ内の割り当ての先頭に徐々に出現してきます。 ただし、リークが少ない場合は、検出できるまで長時間アプリを実行する必要があります。

次のいずれかの方法で、メモリリークをトリガすることも可能です。

  • さまざまな activity 状態で、端末を縦向きから横向きに回転させてから元に戻す動作を複数回行います。 端末を回転させると、ActivityContext、または View オブジェクトのリークがアプリで発生することがよくあります。これは、システムにより Activity が再作成され、これらのいずれかのオブジェクトへの参照がどこか別の場所で保持されている場合、システムはこのようなオブジェクトをガベージ コレクションの対象にできないためです。
  • さまざまな activity 状態で、自分のアプリと別のアプリを切り替えます(ホーム画面に移動してから自分のアプリに戻るなど)。

ヒント: 上述の手順は、monkeyrunner テスト フレームワークを使用しても実行できます。