Memory Profiler は Android Profiler のコンポーネントであり、スタッタリング、フリーズ、さらにはアプリのクラッシュを引き起こす可能性があるメモリリークとメモリチャーンを特定するために役立ちます。アプリのメモリ使用量のリアルタイム グラフが表示され、ヒープダンプの取得、ガベージ コレクションの強制実行、メモリ割り当てのトラッキングを行うことができます。
Memory Profiler を開く手順は次のとおりです。
- [View] > [Tool Windows] > [Profiler] をクリックします(またはツールバーの [Profile] をクリックします)。
- Android Profiler のツールバーで、プロファイリングするデバイスとアプリのプロセスを選択します。USB 経由でデバイスを接続しているにもかかわらずデバイスがリストに表示されない場合は、USB デバッグが有効になっているかを確認します。
- [MEMORY] タイムラインの任意の場所をクリックして Memory Profiler を開きます。
または、コマンドラインで dumpsys を使ってアプリのメモリを調べたり、logcat の GC イベントを表示したりすることもできます。
アプリのメモリをプロファイリングする理由
Android は管理メモリ環境を備えています。アプリに使用されていないオブジェクトが検出されると、ガベージ コレクターは未使用のメモリをリリースしてヒープに戻します。Android が未使用のメモリを検出する仕組みは継続的に改善されていますが、すべての Android バージョンにおいて、ある時点でコードの実行を短時間一時停止する必要があります。ほとんどの場合、そういった一時停止がエクスペリエンスに影響することはありませんが、システムがメモリを収集できる以上の速度でアプリがメモリを割り当てると、コレクターが割り当てに必要なメモリを解放する間、アプリで遅延が発生する場合があります。この遅延により、アプリがフレームをスキップしたり、明らかに速度が低下したりする場合があります。
アプリで速度低下が発生していない場合でも、メモリリークが発生していれば、アプリはバックグラウンドにあってもメモリを保持します。これにより、不必要なガベージ コレクション イベントが強制的に実行され、システム上の残りのメモリ パフォーマンスが低下する可能性があります。最終的には、システムによりアプリプロセスが強制終了され、メモリが回収されます。その後、ユーザーがアプリに戻ったとき、アプリを完全に再起動する必要があります。
こうした問題を防ぐには、Memory Profiler を使用して次の措置を講じる必要があります。
- パフォーマンス上の問題を引き起こす可能性がある、望ましくないメモリ割り当てパターンをタイムラインで探す。
- Java ヒープをダンプして、任意の時点でメモリを不必要に消費しているオブジェクトを特定する。メモリリークの特定には、長期間にわたる複数のヒープダンプが役立ちます。
- ユーザーによる通常の操作と極端な操作の際のメモリ割り当てを記録して、コードが短時間にオブジェクトを割り当てすぎている領域、またはメモリリークが発生するオブジェクトを割り当てている領域を正確に特定する。
アプリのメモリ使用量を削減できるプログラミング方法については、アプリのメモリを管理するをご覧ください。
Memory Profiler の概要
Memory Profiler を最初に開くと、アプリのメモリ使用量の詳細なタイムラインと、ガベージ コレクションの強制実行、ヒープダンプの取得、メモリ割り当ての記録を行えるアクセスツールが表示されます。
図 1 に示すように、Memory Profiler のデフォルト ビューには次の項目が含まれます。
- ガベージ コレクション イベントを強制実行するボタン。
-
ヒープダンプを取得するボタン。
注: メモリ割り当てを記録するボタンは、Android 7.1(API レベル 25)以前を搭載しているデバイスに接続されている場合にのみ、ヒープダンプ ボタンの右側に表示されます。
- プロファイラがメモリ割り当てをキャプチャする頻度を指定するためのプルダウン メニュー。適切なオプションを選択して、プロファイリング中にアプリのパフォーマンスを改善できます。
- タイムラインを拡大または縮小するボタン。
- ライブメモリ データにジャンプするボタン。
- アクティビティの状態、ユーザー入力イベント、画面の回転イベントを示すイベント タイムライン。
- 次の要素が含まれるメモリ使用量のタイムライン。
- 各メモリカテゴリで使用されているメモリ使用量の積み上げ棒グラフ。左の Y 軸はメモリ使用量を、上部の色キーはメモリカテゴリを示しています。
- 割り当てられたオブジェクトの数を表す破線。右の Y 軸はオブジェクトの数を示しています。
- 各ガベージ コレクション イベントのアイコン。
ただし、Android 7.1 以前を搭載しているデバイスを使用している場合は、すべてのプロファイリング データがデフォルトで表示されるわけではありません。「Advanced profiling is unavailable for the selected process」というメッセージが表示された場合は、下記を確認するために詳細なプロファイリングを有効にする必要があります。
- イベント タイムライン
- 割り当てられたオブジェクトの数
- ガベージ コレクション イベント
Android 8.0 以降では、デバッグ可能なアプリに対して詳細なプロファイリングが常に有効になります。
メモリをカウントする方法
Memory Profiler の上部に表示される数値(図 2)は、Android システムに応じて、アプリがコミットしたすべてのプライベート メモリ ページに基づいています。このカウントには、システムまたは他のアプリで共有されるページは含まれません。
メモリカウントのカテゴリは次のとおりです。
- Java: Java または Kotlin コードから割り当てられたオブジェクトのメモリ。
Native: C または C++ コードから割り当てられたオブジェクトのメモリ。
Android フレームワークでは、コードを Java または Kotlin で記述していても、画像アセットやその他のグラフィックを処理するときなどはネイティブ メモリを使用してユーザーのさまざまなタスクが処理されるため、アプリで C++ を使用していない場合でも、使用された一部のネイティブ メモリがここに表示される場合があります。
Graphics: GL サーフェスや GL テクスチャなど、画面にピクセルを表示するためにグラフィック バッファキューに使用されたメモリ(このメモリは専用の GPU メモリではなく、CPU と共有されるメモリであることにご注意ください)。
Stack: アプリのネイティブ スタックと Java スタックの両方によって使用されたメモリ。通常、アプリが実行しているスレッドの数に関係します。
Code: dex バイトコード、最適化またはコンパイルされた dex コード、.so ライブラリ、フォントなど、コードとリソースのためにアプリが使用したメモリ。
Others: アプリによって使用された、カテゴリが不明なメモリ。
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 からフォークされたため、新しい数値はヒープに割り当てられたすべての物理メモリページを表します。したがって、アプリが実際に使用している物理メモリの量が正確に表されます。
メモリ割り当てを表示する
メモリ割り当ては、メモリの各 Java オブジェクトと JNI 参照がどのように割り当てられたかを示します。特に、Memory Profiler はオブジェクト割り当てに関して次の内容を表示できます。
- 割り当てられたオブジェクトの種類とオブジェクトのメモリ使用量。
- 各割り当てのスタック トレースと含まれているスレッド。
- オブジェクトが割り当て解除されたタイミング(Android 8.0 以降を搭載したデバイスを使用している場合のみ)
Java と Kotlin の割り当てを記録するには、[Record Java / Kotlin Allocations] を選択した後、[Record] を選択します。Android 8 以降を搭載したデバイスの場合、Memory Profiler UI は進行中の記録を表示する別の画面に遷移します。記録の上に表示されるミニ タイムラインは操作可能です(たとえば、選択範囲を変更できます)。記録を完了するには、[Stop] を選択します。
Android 7.1 以前では、Memory Profiler は従来の割り当て記録を使用します。この場合、[Stop] をクリックするまで記録がタイムラインに表示されます。
タイムラインで領域を選択すると(または、Android 7.1 以前を搭載しているデバイスで記録セッションを完了すると)、クラス名でグループ化され、ヒープカウントの順に割り当て済みのオブジェクトが表示されます。
割り当て記録を調べる手順は次のとおりです。
- リストに目をとおして、ヒープカウントが著しく大きく、リークが発生している可能性があるオブジェクトを見つけます。既知のクラスを見つけやすくするには、[Class Name] 列の見出しをクリックして、アルファベット順に並べ替えてからクラス名をクリックします。図 3 に示すように、右側に [Instance View] ペインが表示され、そのクラスの各インスタンスが表示されます。
- または、フィルタ アイコン をクリックするか Ctrl+F(Mac の場合は Command+F)を押し、検索フィールドにクラス名またはパッケージ名を入力することで、オブジェクトをすばやく検索できます。プルダウン メニューで [Arrange by callstack] を選択して、メソッド名で検索することもできます。正規表現を使用する場合は、[Regex] のチェックボックスをオンにします。検索クエリで大文字と小文字を区別する場合は、[Match case] のチェックボックスをオンにします。
- [Instance View] ペインでインスタンスをクリックします。ペインの下に [Call Stack] タブが表示され、そのインスタンスが割り当てられた場所と含まれるスレッドが示されます。
- [Call Stack] タブで任意の行を右クリックし、[Jump to Source] を選択してコードをエディタで開きます。
割り当てられたオブジェクト一覧の上にある 2 つのメニューで、調査するヒープと、データの整理方法を選択できます。
左側のメニューで、調査するヒープを選択します。
- default heap: システムによってヒープが指定されていない場合。
- image heap: 起動時にプリロードされたクラスを含むシステム ブートイメージ。この割り当てが変化することはありません。
- zygote heap: Android システムでアプリプロセスがフォークされたコピー オン ライト ヒープ。
- app heap: アプリがメモリを割り当てるプライマリ ヒープ。
- JNI heap: Java Native Interface(JNI)参照の割り当てと開放が行われる場所を示すヒープ。
右側のメニューで、割り当ての配置方法を選択します。
- Arrange by class: クラス名に基づいてすべての割り当てをグループ化。これがデフォルトです。
- Arrange by package: パッケージ名に基づいてすべての割り当てをグループ化。
- Arrange by callstack: すべての割り当てを対応するコールスタックにグループ化。
プロファイリング中にアプリのパフォーマンスを改善する
プロファイリング中にアプリのパフォーマンスを改善するため、Memory Profiler はデフォルトでメモリ割り当てをサンプリングします。API レベル 26 以上を搭載しているデバイスでテストする際は、[Allocation Tracking] プルダウンを使用してこの動作を変更できます。使用できるオプションは次のとおりです。
- Full: メモリ内のすべてのオブジェクト割り当てをキャプチャする。これは Android Studio 3.2 以前のデフォルトの動作です。多数のオブジェクトを割り当てるアプリの場合は、プロファイリング中にアプリが目に見えて遅くなることがあります。
- Sampled: メモリ内のオブジェクト割り当てを定期的にサンプリングする。これはデフォルトのオプションで、プロファイリング時のアプリのパフォーマンスへの影響は小さくなります。短時間に多数のオブジェクトを割り当てるアプリでは、依然として速度が目に見えて低下する可能性があります。
- Off: アプリのメモリ割り当てのトラッキングを停止する。
グローバル JNI 参照の表示
Java Native Interface(JNI)は、Java コードとネイティブ コードが互いに呼び出せるようにするフレームワークです。
JNI 参照はネイティブ コードによって手動で管理されるため、ネイティブ コードによって使用される Java オブジェクトが長期間存在し続ける可能性があります。JNI 参照が最初に明示的に削除されずに破棄されると、Java ヒープ上の一部のオブジェクトに到達不能となる可能性があります。また、グローバル JNI 参照制限を使い切ることも可能です。
このような問題のトラブルシューティングには、Memory Profiler の [JNI heap] ビューを使用してすべてのグローバル JNI 参照を表示し、Java タイプとネイティブ コールスタックでフィルタします。この情報から、グローバル JNI 参照がいつどこで作成、削除されるのかを確認できます。
アプリを実行している状態で検査対象のタイムライン部分を選択し、クラスリストの上にあるプルダウン メニューで [JNI heap] を選択します。これで、通常のようにヒープ内のオブジェクトを検査できます。[Allocation Call Stack] タブでオブジェクトをダブルクリックすると、コード内のどこで JNI 参照の割り当てと解除が行われているかを確認できます(図 4 参照)。
アプリの JNI コードに対するメモリ割り当てを調べるには、Android 8.0 以降を搭載しているデバイスにアプリをデプロイする必要があります。
JNI について詳しくは、JNI に関するヒントをご覧ください。
Native Memory Profiler
Android Studio Memory Profiler に組み込まれたアプリ用の Native Memory Profiler Android 10 以降を搭載した物理デバイスと仮想デバイスにデプロイされます。
Native Memory Profiler は、特定の期間のネイティブ コードのオブジェクトの割り当てと割り当て解除を追跡し、次の情報を提供します。
- Allocations: 選択した期間中に
malloc()
またはnew
演算子によって割り当てられたオブジェクトの数。 - Deallocations: 選択した期間中に
free()
またはdelete
演算子によって割り当て解除されたオブジェクトの数。 - Allocations Size: 選択した期間中のすべての割り当ての合計サイズ(バイト単位)。
- Deallocations Size: 選択した期間中のすべての解放されたメモリの合計サイズ(バイト単位)。
- Total Count: [Allocations] 列の値から [Deallocations] 列の値を引いた値。
- Remaining Size: [Allocations Size] 列の値から [Deallocations Size] 列の値を引いた値。
Android 10 以降を搭載したデバイスでネイティブ割り当てを記録するには、[Record native allocations] を選択した後、[Record] を選択します。記録は [Stop] をクリックするまで継続されます。停止すると、Memory Profiler UI はネイティブ記録を表示する別の画面に遷移します。
Android 9 以前では、[Record native allocations] オプションは使用できません。
デフォルトでは、Native Memory Profiler は 32 バイトのサンプルサイズを使用します。32 バイトのメモリが割り当てられるたびに、メモリのスナップショットが作成されます。サンプルサイズが小さいほどスナップショットの頻度は高くなり、メモリ使用量に関してより正確なデータが得られます。サンプルサイズが大きいほどデータの精度は低くなりますが、システムにおけるリソース消費量を削減し、記録時のパフォーマンスが改善されます。
Native Memory Profiler のサンプルサイズを変更するには:
- [Run] > [Edit Configurations] の順に選択します。
- 左側のペインでアプリ モジュールを選択します。
- [Profiling] タブをクリックし、[Native memory sampling interval (bytes)] というラベルのフィールドにサンプルサイズを入力します。
- 再度アプリをビルドして実行します。
ヒープダンプの取得
ヒープダンプは、ヒープダンプの取得時にアプリのどのオブジェクトがメモリを使用しているかを示します。特に、長いユーザー セッションの後は、メモリに存在しないと思われるオブジェクトが引き続きメモリにあることが示されるため、ヒープダンプによりメモリリークの特定が容易になります。
ヒープダンプを取得したら、次の内容を確認できます。
- アプリが割り当てたオブジェクトのタイプと各タイプのオブジェクト数。
- 各オブジェクトが使用しているメモリ量。
- コードで各オブジェクトへの参照が保持される場所。
- オブジェクトが割り当てられた場所のコールスタック(現在、割り当ての記録中にヒープダンプを取得した場合、Android 7.1 以前を使用して取得されたヒープダンプのみでコールスタックを利用できます)。
ヒープダンプをキャプチャするには、[Capture heap dump] をクリックした後、[Record] を選択します。ヒープをダンプする際に Java メモリの使用量が一時的に増加する場合がありますが、問題はありません。ヒープダンプはアプリと同じプロセスで発生し、データを収集するためにいくらかのメモリが必要になるためです。
プロファイラがヒープダンプのキャプチャを完了すると、Memory Profiler UI はヒープダンプを表示する別の画面に遷移します。
より厳密なタイミングでダンプを作成する必要がある場合は、アプリコード内の重要な場所で dumpHprofData()
を呼び出してヒープダンプを作成できます。
クラスの一覧には、次の情報が表示されます。
- Allocations: ヒープ内の割り当ての数。
Native Size: このオブジェクト タイプで使用されるネイティブ メモリの合計量(バイト単位)。この列は Android 7.0 以降でのみ表示されます。
Android では、
Bitmap
などのフレームワーク クラスにネイティブ メモリが使用されているため、ここでは Java で割り当てられたオブジェクト用のメモリが表示されます。Shallow Size: このオブジェクト タイプによって使用されている Java メモリの合計量(バイト単位)。
Retained Size: このクラスのすべてのインスタンスのために保持されているメモリの合計サイズ(バイト単位)。
割り当てられたオブジェクト一覧の上にある 2 つのメニューで、調査するヒープダンプと、データの整理方法を選択できます。
左側のメニューで、調査するヒープを選択します。
- default heap: システムによってヒープが指定されていない場合。
- app heap: アプリがメモリを割り当てるプライマリ ヒープ。
- image heap: 起動時にプリロードされたクラスを含むシステム ブートイメージ。この割り当てが変化することはありません。
- zygote heap: Android システムでアプリプロセスがフォークされたコピー オン ライト ヒープ。
右側のメニューで、割り当ての配置方法を選択します。
- Arrange by class: クラス名に基づいてすべての割り当てをグループ化。これがデフォルトです。
- Arrange by package: パッケージ名に基づいてすべての割り当てをグループ化。
- Arrange by callstack: すべての割り当てを対応するコールスタックにグループ化。このオプションは、割り当ての記録中にヒープダンプを取得する場合にのみ機能します。ただし、記録の開始前にヒープにオブジェクトが存在する可能性があるため、これらの割り当ては最初にクラス名ごとに表示されます。
デフォルトでは、一覧は [Retained Size] 列で並べ替えられます。別の列の値で並べ替えるには、列の見出しをクリックします。
クラス名をクリックして右側の [Instance View] ウィンドウを開きます(図 6 参照)。それぞれのインスタンスには次のものが含まれます。
- Depth: GC ルートから選択済みのインスタンスへの最小ホップ数。
- Native Size: このインスタンスのネイティブ メモリ内でのサイズ。この列は Android 7.0 以降でのみ表示されます。
- Shallow Size: このインスタンスの Java メモリ内でのサイズ。
- Retained Size: このインスタンスがドミネートするメモリのサイズ(ドミネーター ツリーに従う)。
ヒープを調べる手順は次のとおりです。
- リストに目をとおして、ヒープカウントが著しく大きく、リークが発生している可能性があるオブジェクトを見つけます。既知のクラスを見つけやすくするには、[Class Name] 列の見出しをクリックして、アルファベット順に並べ替えてからクラス名をクリックします。図 6 に示すように、右側に [Instance View] ペインが表示され、そのクラスの各インスタンスが表示されます。
- または、フィルタ アイコン をクリックするか Ctrl+F(Mac の場合は Command+F)を押し、検索フィールドにクラス名またはパッケージ名を入力することで、オブジェクトをすばやく検索できます。プルダウン メニューで [Arrange by callstack] を選択して、メソッド名で検索することもできます。正規表現を使用する場合は、[Regex] のチェックボックスをオンにします。検索クエリで大文字と小文字を区別する場合は、[Match case] のチェックボックスをオンにします。
- [Instance View] ペインでインスタンスをクリックします。ペインの下に [References] タブが表示され、そのオブジェクトへのすべての参照が示されます。
または、インスタンス名の横にある矢印をクリックしてそのすべてのフィールドを表示してから、フィールド名をクリックしてすべての参照を表示します。フィールドのインスタンスの詳細を表示する場合は、フィールドを右クリックして、[Go to Instance] を選択します。
- [References] タブで、メモリリークが発生している可能性のある参照を特定するには、タブを右クリックして [Go to Instance] を選択します。これにより、ヒープダンプから対応するインスタンスが選択され、そのインスタンス データが表示されます。
ヒープダンプでは、次のいずれかが原因で生じたメモリリークに注目します。
Activity
、Context
、View
、Drawable
と、Activity
またはContext
コンテナへの参照を保持している可能性があるその他のオブジェクトへの長期的参照。Activity
インスタンスを保持できる、Runnable
などの非静的内部クラス。- 必要以上に長くオブジェクトを保持するキャッシュ。
ヒープダンプを HPROF ファイルとして保存する
ヒープダンプを取得した後は、Memory Profiler の実行中にのみデータが表示されます。プロファイリング セッションを終了すると、ヒープダンプは失われます。そのため、後でレビューするためにヒープダンプを保存する場合は、ヒープダンプを HPROF ファイルにエクスポートします。Android Studio 3.1 以前では、タイムラインの下のツールバーの左側に [Export capture to file] ボタン があります。Android Studio 3.2 以降では、[Sessions] ペインの各 [Heap Dump] エントリの右側に [Export Heap Dump] ボタンがあります。表示される [Export As] ダイアログで、ファイル名拡張子 .hprof
を付けてファイルを保存します。
jhat など別の HPROF アナライザーを使用するには、HPROF ファイルを Android 形式から Java SE HPROF 形式に変換する必要があります。そのために、android_sdk/platform-tools/
ディレクトリにある hprof-conv
ツールを使用します。元の HPROF ファイルと変換済みの HPROFR ファイルの書き出し先の 2 つの引数を指定して、hprof-conv
コマンドを実行します。次に例を示します。
hprof-conv heap-original.hprof heap-converted.hprof
ヒープダンプ ファイルをインポートする
HPROF(.hprof
)ファイルをインポートするには、[Start a new profiling session] をクリックします。[Sessions] ペインで [Load from file] を選択し、ファイル ブラウザからファイルを選択します。
HPROF ファイルをファイル ブラウザからエディタ ウィンドウにドラッグしてインポートすることもできます。
Memory Profiler でのリーク検出
Memory Profiler でヒープダンプを分析する場合、アプリの Activity
インスタンスと Fragment
インスタンスでメモリリークが発生していると考えられるプロファイリング データをフィルタできます。
フィルタで表示されるデータの種類は、次のとおりです。
- 破棄されているが、まだ参照されている
Activity
インスタンス。 - 有効な
FragmentManager
がないが、まだ参照されているFragment
インスタンス。
次のような状況では、フィルタによって誤検出が発生することがあります。
Fragment
が作成されたが、まだ使用されていない。Fragment
がキャッシュされているが、FragmentTransaction
の一部ではない。
この機能を使用するには、まずヒープダンプを取得するか、Android Studio にヒープダンプ ファイルをインポートします。メモリリークが発生している可能性があるフラグメントとアクティビティを表示するには、図 7 に示すように、Memory Profiler のヒープダンプ ペインで [Activity/Fragment Leaks] チェックボックスをオンにします。
メモリをプロファイリングするためのテクニック
Memory Profiler を使用する際は、アプリコードに過度な負荷をかけてメモリリークを強制的に発生させる必要があります。アプリでメモリリークを発生させるには、ヒープを調査する前にしばらくの間アプリを実行するという方法があります。リークは、ヒープ内の割り当ての先頭に徐々に出現してきます。ただし、リークが少ない場合は、検出できるまで長時間アプリを実行する必要があります。
次のいずれかの方法で、メモリリークをトリガーすることも可能です。
- さまざまなアクティビティの状態で、デバイスを縦向きから横向きに回転させてから元に戻す動作を何度か行う。デバイスを回転させると、アプリがスマートフォンのバッテリーを
Activity
,Context
、またはView
オブジェクトは、Activity
が再作成され、 どこか別の場所でこれらのオブジェクトの 1 つへの参照を保持している場合、システムはその参照をガベージ コレクションの対象にできません。 - さまざまなアクティビティの状態で、自分のアプリと別のアプリを切り替える(ホーム画面に移動してから自分のアプリに戻るなど)。
ヒント: 上述の手順は、monkeyrunner テスト フレームワークを使用しても実施できます。