Android はアプリケーションのメモリをどのように分析しますか (16) - AS を使用して Android ヒープを表示する
先ほど、jdb と VS コードを使用してアプリケーション スタック関連のコンテンツを表示する方法を最初に紹介しました。
この記事では、ヒープの内容を確認する方法を紹介します。について:
- ヒープ内のオブジェクトは何ですか?
- ヒープ内のオブジェクトは誰によって割り当てられますか
- ヒープ内のオブジェクト間の参照関係は何ですか?
ヒープ内にあるオブジェクトとその参照関係 - ヒープ ダンプを使用する
現在のヒープ内のオブジェクトを表示するには、ツールを使用してヒープ データをダンプする必要があります。
次にAndroid Studio付属のメモリプロファイラーを使って操作していきます。
ステップ 1: Android プロファイラーを開く
Android Studio では、以下の手順に従ってメモリ プロファイラーを開くことができます。
上の図では 2 つのオプションが表示されており、次のように説明されています。
- profile xxx with low overhead: パフォーマンス アナライザーでは、CPU パフォーマンス アナライザーと
メモリ アナライザーのみが有効になります。メモリ アナライザーでは、ネイティブ割り当ての記録のみが有効になります
(つまり、ネイティブ割り当てを記録する機能が有効になります)。
完全なデータを含むプロファイル xxx: CPU アナライザー、メモリ アナライザー、消費電力アナライザーを含むすべてのアナライザーを有効にします。
注: 上図のアイコンから開始することに加えて、メニューから開始することもできます: [実行] -> [プロファイル] を選択し、
パフォーマンスを分析するモジュールを選択します。
注: パフォーマンスの低いコンピュータでは、Android プロファイラを単独で実行できます。以下の通り:
Android Studio インストール ディレクトリ/bin/profiler.xx (Mac、Linux の場合は profiler.sh、Windows の場合は profiler.exe)
起動後は以下のようになります
注: セッションはパフォーマンス分析を表すため、Android Profiler で新しいセッションを作成し、別のアプリケーション プロセスを選択できます。以下に示すように:
ステップ 2: メモリ プロファイラを開く
上の図で、メモリの任意の領域をクリックしてメモリ プロファイラを開きます。各エリアの具体的な意味を次の図に示します。
上記の通り、それぞれの種類について以下のように説明していますので、本シリーズの第1回記事「Androidでアプリケーションメモリを解析する方法(1) - メモリの概要」 http://t.csdn.cn/HN1Ma も参照してください。
- Java: Java または kotlin によって割り当てられたオブジェクトのメモリ
- ネイティブ: C または C++ によって割り当てられたメモリ
- グラフィックス: GL サーフェスや GL テクスチャなどのピクセルを画面に表示するためにグラフィックス バッファー キューによって使用されるメモリ。これらは GPU 固有のメモリではなく、CPU と共有されるメモリです
- スタック: Java スタックとネイティブ スタック メモリ。これは実行中のスレッドの数に関係します。
- コード: アプリケーションがコードやリソース (dex バイトコード、ライブラリ、フォントなど) を処理するために使用するメモリ。
- その他: メモリのカテゴリを特定できません
- 割り当て済み: アプリケーションによって割り当てられたオブジェクトの数。上の図では、N/A です。数えられる場合は点線が表示され、Y 軸が画像の右側に対応し、オブジェクトの数を示します。
ステップ 3: キャプチャ ヒープ ダンプを使用する
ヒープ内にオブジェクトがいくつあるかを確認するには、キャプチャ ヒープ ダンプを使用して現在のヒープをキャプチャします。以下に示すように:
上の図からわかるように、合計 727 のクラスがあり、すべてのオブジェクトがクラス名に従ってリストにリストされています。
上図のいくつかのマークは次のように説明されています。
-
マーカー 1: 表示する別のヒープを選択します。次のヒープを表示できます
- イメージ ヒープ: 起動時にプリロードされたクラスを含むイメージ ヒープ。ここでの割り当ては移動したり消えたりしません。
- zygote ヒープ: zygote プロセスから継承された zygote ヒープ (システム リソースやクラス ライブラリを含む)。
- アプリ ヒープ: アプリケーションによって割り当てられたメイン ヒープ
- JNI ヒープ: jni によって参照されるヒープ
注: これら 4 つのヒープを理解する方法については、この記事の後半を参照してください: イメージ ヒープ、ザイゴット ヒープ、アプリ ヒープ、JNI ヒープを理解する方法
-
マーク 2: 次のようなさまざまな並べ替え方法を選択します。
- クラス順に並べ替え: クラス名で並べ替えます。
- パッケージ順に並べ替え:登録順に並べ替え
- コールスタックで並べ替え: コールスタックで並べ替えます。
注: コール スタックによって並べ替えられたこの関数は、キャプチャ ヒープ ダンプではサポートされていません。この関数をサポートするには、以下を使用する必要があります: java/kotlin 割り当てを記録する。以下を参照してください: ヒープ内のオブジェクトを割り当てるのは誰ですか?
-
マーク 3: 次のようなクラスの条件付きフィルタリング:
- すべてのクラスを表示: すべてのクラスを表示
- show activity/fragments クラス: 可能性のあるアクティビティとフラグメントのリークを表示します。
注:ここでは「可能」という言葉が使われています。実際、メモリ プロファイラーによって示されるリークは、必ずしも実際のリークではありません
- show projectclasses: このプロジェクト内のクラスを表示します。
-
マーク4:Allocationsは割り当ての数を示しており、例えば1行目は、MaterialTextViewが1回割り当てられている、つまりオブジェクトが割り当てられていることを示している。
-
マーク 5: ネイティブ サイズは、オブジェクトのネイティブ サイズを示します。Java コードまたは kotlin コードしかありませんが、Java は jni を使用してネイティブ メモリを操作できるため、場合によっては依然としてネイティブ サイズが存在します。上図の最初の行は、ネイティブ サイズが 0 であることを示しています。
-
マーク 6: シャロー サイズはオブジェクト自体のサイズを示し、フラット サイズとも呼ばれます。これには内部参照オブジェクトのサイズは含まれません。
-
マーク 7: 保持サイズは、オブジェクト自体のサイズに内部参照オブジェクトのサイズを加えたサイズを示します。これは直接的に次のように理解できます: オブジェクトがリサイクルされると、ヒープのサイズが解放されます。上図の最初の行は、MaterialTextView がリサイクルされると、10381 バイトが解放されることを示しています。
注: オブジェクト A がオブジェクト E および C を参照し、オブジェクト B もオブジェクト E および C を参照する場合。それでは、オブジェクト A の保持サイズには E と C が含まれるでしょうか? B オブジェクトの保持サイズには E と C が含まれますか? 保持サイズの計算については、Shallow Sizeと保持サイズの計算方法を参照してください。
- マーク 8: 検索ボックス。後の 2 つのチェック ボックスは、大文字と小文字を区別するかどうか、および正規表現を使用するかどうかを示します。
ステップ4: 各オブジェクトの参照関係を確認する
参照関係を説明するために、テスト用のリンク リストを作成してみましょう。次のように
//首先定义一个测试类
public class WanbiaoTest{
public String value = "wanbiao_test";
public WanbiaoTest next ;
}
//链表的头,用字母o表示
private WanbiaoTest o ;
//构建测试链表
public void do(){
for(int i=0;i<10;++i){
if( o == null){
o = new WanbiaoTest();
}else{
WanbiaoTest p = o;
while(p.next != null){
p = p.next;
}
p.next = new WanbiaoTest();
}
}
}
上記のコードを実行した後、次に示すように、最初と 2 番目の手順に従ってヒープをダンプします。次に、図の手順に従って参考資料を表示します。
上の写真では。
- クラス名を入力すると、探しているクラスをすぐに見つけることができます。
- クラス名をクリックすると、オブジェクトのリストが表示されます。
- オブジェクトリストには、さまざまなオブジェクトが表示されます。WanbiaoTest オブジェクトはリンクされたリストに従って編成されているためです。したがって、「深さ」列にはさまざまな深さが表示されます。
- 任意のオブジェクトを選択すると、そのオブジェクトの詳細情報が右側に表示されます。
- 参照列をクリックして参照を表示します。
図からわかるように、選択したオブジェクトは常に next を通じて参照されます。最上位の WanbiaoTest は、MainActivity に存在する o によって参照されます。MainActivity オブジェクトは、ActivityThread$ActivityClient オブジェクト内に存在します。
参照チェーン全体は以下のようになります
特定のオブジェクトを表示したい場合は、オブジェクトを右クリックして「インスタンスに移動」を選択することもできます。
注: 「最も近い GC ルートへの参照チェーン」を表示することに加えて、すべての参照を表示することもできます。つまり、オブジェクトによって参照されているすべてのオブジェクトを表示するには、削除: 最も近い GC ルートのみを表示します。
上記は、オブジェクト間の参照関係を表示する方法を示しているだけです。では、メモリがリークしているかどうかを判断するにはどうすればよいでしょうか? この質問に答えるためには、まずこれらのオブジェクトが誰によって割り当てられているかを確認する方法を学ぶ必要もあります。
ヒープ内のオブジェクトを誰が割り当てるか - レコード java/kotlin 割り当てを使用
オブジェクトを割り当てたユーザーを知りたい場合は、オブジェクトを割り当てたコール スタックを知る必要があるため、次の手順に従ってアプリケーションのコール スタック情報を記録できます。次のように:
ステップ 1: コールスタック情報を記録します。
以下に示すように記録を開始します。
以下に示すように録音を終了します
録音結果は以下の通り
詳細情報は図中にマークしてありますが、マークのない箇所は以前紹介したものです。
ここでもう 1 つ注意すべき点があります。録音終了ボタンの横にドロップダウン ラジオ ボタンがあり、現在は [フル] になっています。使用可能なオプションは次のとおりです。
- フル: すべてのオブジェクト割り当てを記録します。これにより、アプリのパフォーマンスが大幅に低下します。
- サンプル: 特定のサンプリング間隔 (サンプリング レート) でメモリ割り当てをサンプリングします。サンプリング間隔とサンプリングレートの詳細については、「Android でアプリケーションのメモリを分析する方法 (13) - perfetto」を参照してください。http://t.csdn.cn/laqYB : heapprofd のパフォーマンスが優れている理由
ステップ 2: オブジェクト呼び出しスタックを表示する
表示するクラスを選択するとオブジェクトリストが表示されるので、オブジェクトを選択します。以下に示すように
この時点で、オブジェクトのコールスタック情報を表示できます。
先ほど紹介したテーブルビュー以外にもフレームグラフで見ることもできます。テーブルの横にある [Visualization] を選択して、フレーム グラフ モードに切り替えます。以下に示すように
上のフレーム グラフでは、関数のスパンが大きいほど、選択に対応する値 (つまり、割り当て数、割り当てサイズ、合計残りサイズ、合計残り数のいずれか) が大きくなります。
それ以来、このツールがヒープ内のオブジェクト、オブジェクトの参照関係、オブジェクトのコール スタック情報を表示する方法を導入してきました。
次に、具体的な例として 2 つの小さな例を使用します。
上記ツールの総合活用 - 実戦1、活動漏洩
この例では、アクティビティ リークを手動で作成しました。次のシナリオを考えます。
- DeviceManager というデバイス マネージャーがあり、これはシングルトン オブジェクトです。
- デバイス マネージャーには 2 つのインターフェイスがあり、デバイス ステータス リスナーの登録と破棄に使用されます。次のように
public class DeviceManager {
//单例对象
private DeviceManager() {
}
private static class DeviceManagerHolder {
private static final DeviceManager INSTANCE = new DeviceManager();
}
public static final DeviceManager getInstance() {
return DeviceManagerHolder.INSTANCE;
}
//定义监听器接口
public interface DeviceChangedListener{
void onChanged(int oldStatus,int newStatus);
}
private ArrayList<DeviceChangedListener> listeners = new ArrayList<>();
public void addListener(DeviceChangedListener listener){
if(!listeners.contains(listener)){
listeners.add(listener);
}
}
public void removeListener(DeviceChangedListener listener){
listeners.remove(listener);
}
}
- ビジネス オブジェクト Class Task が完成したので、次の操作を実行する必要があります。タスクが作成されたら、DeviceManager にリスナーを登録します。タスクが破棄されたら、リスナーを DeviceManager からログアウトします。コードは以下のように表示されます。
//Task自我监听,Device的状态改变
//看上去这是一个较好的封装
public class Task implements DeviceManager.DeviceChangedListener {
private Runnable mTaskRunnable;
public Task(Runnable task){
mTaskRunnable = task;
DeviceManager.getInstance().addListener(this);
task.run();//运行其他业务
}
@Override
protected void finalize(){
//回收的时候,注销掉监听器
DeviceManager.getInstance().removeListener(this);
}
@Override
public void onChanged(int oldStatus, int newStatus) {
Log.i("Task","oldStatus = "+oldStatus+"newStatus = "+newStatus);
}
}
- 次に、ActivityのonCreateでタスクを作成し、実行を開始します。コードは以下のように表示されます。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//执行业务
class TaskRunable implements Runnable{
private Context mContext;
public TaskRunable(Context context){
mContext = context;
}
@Override
public void run() {
// do something
}
}
Task t = new Task(new TaskRunable(this));
}
- 次にユーザーの操作をシミュレーションします。
画面を上下左右に数回反転させます。次に、GC 呼び出しを強制します。以下に示すように
次に、キャプチャ ヒープ ダンプを使用して、メモリ リークがあるかどうかを確認します。以下に示すように
上の図から、メモリ プロファイラーがアクティビティ リークを引き起こしていることがわかります。
考察: メモリ プロファイラーがアクティビティのリークをチェックできる理由。
回答: アクティビティが破棄された後でも、GC ルートからアクセスできる場合は、リークが発生していることを意味するためです。
ここでは、コードのどこでリークが発生しているのかがわかりません。リークを見つけるために、次の図の手順に従います。
上の図から、次の呼び出しチェーンがわかります。
このことから、アクティビティリークの理由がわかりました。TaskRunnable はその強力な参照を保持しています。では、それを弱い参照に変更すると、改善されるのでしょうか? 次のように:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//执行业务
class TaskRunable implements Runnable{
//将强引用改为弱引用
private WeakReference<Context> mContext;
public TaskRunable(Context context){
mContext = new WeakReference<>(context);
}
@Override
public void run() {
// do something
}
}
}
上記コードに変更後、再度メモリプロファイラを使用してヒープダンプを実行してください。次の結果が表示されます
とても残念です!!!未だに漏洩の解決には至っていません。この問題はどこで発生するのでしょうか? 上記の手順に従って参照を表示し、次の図を取得します。
図からわかるように、TaskRunable オブジェクト内に MainActivity を指す this$0 への参照があります。
おお!!, この$0は内部クラスから外部オブジェクトへの参照であることが判明したので、この問題を解決するには
TaskRunableクラスをMainActivityの外に移動します。
再度ヒープ ダンプを実行します。今回はアクティビティのリークはありません。!!
上記ツールの総合活用 - 実戦2、オブジェクト漏洩
上記の実戦1では本当に漏れはないのでしょうか?TaskRunnable に大きなオブジェクトが含まれている場合はどうなるでしょうか? どのようなパフォーマンスを発揮するのでしょうか?
TaskRunnable に大きなオブジェクトが含まれていることをシミュレートするために、次のように内部的に整数配列を追加します。
class TaskRunable implements Runnable{
private WeakReference<Context> mContext;
//1024*4=4096byte 等于4KB.模拟一个大对象
private int[] values = new int[1024];
public TaskRunable(Context context){
mContext = new WeakReference<>(context);
}
@Override
public void run() {
// do something
}
}
一連のメモリ テストの結果、以下に示すように、Java メモリが増加しており、GC がそれをリサイクルできなくなっていることがわかりました (青色の部分)。
※ Android アプリのメモリのテスト方法については、時間があるときに改めて書きますが、このシリーズではメモリの解析方法を中心に説明します。幸いなことに、メモリのテストは比較的簡単です。実際、http://t.csdn.cn/ovFmOをフォローしてください。リアルタイム監視スクリプトを作成するだけです。もちろん、procstats サービスと組み合わせることもできます。http: //t.csdn.cn/4Qp9tの最後のセクションにある procstats の推奨事項を参照してください。
このメモリの問題を見つけるために、ヒープ ダンプを使用して内部の詳細を調べます。以下に示すように:
上の写真からわかるように、物体が漏洩した形跡はありません。では、この種の漏れの問題はどのように見つければよいのでしょうか? Java メモリが増加し続けていることが観察されており、ヒープ内のオブジェクトが何度も割り当てられ、解放されていないのではないかと考えられます。これを行うには、[割り当て] (つまり、割り当ての数) をクリックして、逆の順序で表示します。
上の図からわかるように、最も多くの割り当てが行われているカテゴリは、int[]、WeakReference、FinalizerReference、TaskRunable、および Task です。
Shallow サイズから最も大きいのは int[] であることがわかり、メモリ リークを引き起こしているオブジェクトは int[] であることがほぼ確実です。上記の学習から、その参照チェーンを見てみましょう。次のように:
図からわかるように、リークは、DeviceManager に登録されているオブジェクトが時間内にログアウトされないことが原因で発生します。
この問題を解決するには、DeviceManager は必要に応じて、使用されなくなったリスナーを削除する必要があります。タスクのファイナライズ関数から、クラスが使用されなくなったらリスナーを GC によって削除する必要があることがわかります。したがって、DeviceManager のリスナーは弱参照として設計されています。コードはシンプルすぎるため、添付されなくなりました。
弱い参照に変更すると、オブジェクトはリークされなくなります。
注: 上記のコードは依然としてエンジニアリング実践とみなされません。タスクが使用されなくなり、GC がまだリサイクルされていない場合、実際に DeviceManager にステータス変化があればタスクに通知されますが、このときタスクに対応する処理ロジックがあると問題が発生する可能性があります。したがって、ここでは注意が必要です。ただし、上記の例は、メモリ ツールの使用法を説明するためだけにすぎません。
上記の 2 つの例は、あまりにも明白で明瞭です。これらは、メモリ プロファイラーの使用法を説明することのみを目的としています。実際、実際のメモリ関係は上記よりもはるかに複雑である可能性があります。ただし、記事が長くなるのでこれ以上は省略しますので、機会があれば後日追記したいと思います。
この記事を終える前に、答えなければならない小さな質問が 2 つあるようです。まず、Android のイメージ ヒープ、ザイゴット ヒープ、アプリ ヒープ、および JNI ヒープとは何ですか。2 番目: 保持サイズの計算方法
如何理解Image heap,zygote heap,app heap,JNI heap
これら 4 つのヒープを説明するために、起動から始めて、次のように簡単に要約します。
-
Android システムが起動すると、zygote プロセスと呼ばれるプロセスが作成されます。zygote プロセスが初めて開始されると、一部のシステム リソースを含む多くのリソースがロードされます。それからまた走ります。
-
Android が別のプロセスを開始したい場合、zygote のように最初から開始することはできません。代わりに、zygote プロセスを直接フォークします。次に、ザイゴート プロセスの一部のリソースが再利用され、ザイゴートの特別なヒープが再利用されます。このヒープは、最初のステップでシステム リソースがロードされるヒープです。このヒープの名前は、ザイゴット ヒープと呼ばれます。アプリケーションがこのヒープの内容を変更する必要がある場合、アプリケーションはこの時点で新しいヒープを作成し、次にザイゴット ヒープの内容を新しいヒープにコピーしてから変更します。つまり、コピーオンライトです。
-
Android 仮想マシンの起動時に、最適化されたバイトコードをロードする必要があります。これらの最適化されたバイトコードは、将来直接使用できるように特別なヒープにマップされます。このヒープはイメージ ヒープと呼ばれます。
-
Android アプリケーションの起動後、オブジェクトを割り当てる必要があり、その後オブジェクトはアプリ ヒープから割り当てられます。このヒープはアプリケーションのメイン ヒープです。
-
Android アプリケーションが使用中に JNI 参照を使用すると、これらの JNI 参照は別のヒープに配置され、このヒープが JNI ヒープになります。
浅いサイズと保持サイズの計算方法
浅いサイズ: 内部参照オブジェクトのサイズを計算しない、オブジェクト自体のサイズ。次のように:
class A{
int a;
A aInner;
}
次に、A オブジェクトの浅いサイズ = 4 (a は int で 4 バイトを占有) + 4 (aInner は参照で 4 バイトを占有) + 8 (Object オブジェクト内のフィールド) = 16 バイトとなります。
注: 4 バイトの整列を保証するために、4 バイトの倍数でない場合、4 バイトの倍数になる場合があります。なぜ 4 バイト アラインメントなのか? これは、メモリアクセス効率を向上させるというメモリバスの目的によるものです。ここには表示されていませんが、Baidu で自分で検索できます
保持サイズ: オブジェクト自体のサイズに内部参照オブジェクトのサイズを加えたもの。しかし、1 つのオブジェクトが複数のオブジェクトによって参照されている場合はどうなるでしょうか? 以下に示すように
この問題を解決するには、ドミネーター ツリーについて知る必要があります。
その定義は次のとおりです。有向グラフでは、ソース点から点 B まで、とにかく点 A を通過する必要がある場合、A は B の支配的な点であり、A は B を支配していると言われます。B に最も近い支配点を直接支配点と呼びます。上に示したように。B は D と G の直接の制御点です。
上記の関係より、次のようなドミネーターツリーグラフが得られます(右図)
上の写真の説明:
- D が直接制御: E、F
- B が直接制御: D、G、H
E 保持サイズ
= E 浅いサイズ
F 保持サイズ = F 浅いサイズ
H 保持サイズ = H 浅いサイズ
G 保持サイズ = G 浅いサイズ
D 保持サイズ = E 浅いサイズ + F 保持サイズ + D 浅いサイズ
B 保持サイズ = D 保持サイズ + H 保持サイズ + G 保持サイズ + B 浅いサイズ
これでこの記事は終わりです。
次の記事では、やはり Java ヒープ メモリであり、他の 2 つのツール、perfetto と mat をヒープ メモリの分析に使用します。乞うご期待。