JVMTI for Android パフォーマンス監視の 4 つの基本操作と 2 つの興味深い結論

1.JVMTIとは

JVMTI は、JVM の開発と監視に使用されるプログラム インターフェイスで、JVM の内部状態を調査し、JVM アプリケーションの実行を制御できます。利用可能な機能には以下が含まれますが、これらに限定されません。

  • デバッグ、ブレークポイント
  • メモリ割り当て、リサイクルを監視する
  • 解析スレッド作成終了
  • カバレッジ分析
  • スタック管理
  • バイトコードフックなど

すべての JVM 実装が JVMTI をサポートしているわけではなく、Android は 8 以降に追加された JVMTI 実装であることに注意してください。

中国の文書

https://blog.caoxudong.info/blog/2017/12/07/jvmti_reference#1.1

英語文書

https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#SpecificIntro

模式図は次のようになります。中間エージェントとみなすことができます。

f936ccc76a648280facad34a06380eb3.png

2. JVMTIと接触した理由

Android のパフォーマンス監視の問題に関する最近の調査のため、私は偶然このブラック テクノロジーに遭遇しました。

第三に、何ができるか

JVMTI は開発者にとっての JVM のバックドアであり、これを使用すると、オブジェクト割り当て、ガベージ コレクション、スレッド スケジューリング、リアルタイム デバッグなど、JVM の実行ステータスをリアルタイムで検出できます。

4.何に使いたいですか

  1. オブジェクト割り当てのリアルタイム収集 (オブジェクト割り当ての数とサイズを含む)
  2. GC イベントを記録してメモリ リークの分析に役立てる
  3. スレッドのアクティビティをログに記録する
  4. メソッド呼び出しを記録します (メソッド呼び出しと終了時の実行時間を記録します)

5. 上記の機能目標を達成する方法


JVM は C で書かれているため、監視したい場合は、C/C++ でダイナミック リンク ライブラリを作成し、実行時にこのライブラリをアタッチする必要があります。このライブラリには、次のように、JVMTI のエントリ ポイントとしてメイン コールバック関数が必要です。

extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved)

このエントリ関数では、監視する必要があるイベントをカスタマイズします。イベント コールバックは合計 32 個あり、必要に応じてカスタマイズできます。コールバック イベントが多いほど、JVM のパフォーマンスに与える影響は大きくなります。たとえば、メモリ割り当てイベントは常に発生し、評価率は非常に高くなります。1 秒あたり 1,000 回に達する場合もあり、メソッド呼び出しの頻度は 1 秒あたり 10,000 回に達する場合もあります。

以下は、デモでの Agent_OnAttach メソッドです。


/**
 * Agent attch 回调
 */
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved) {
    //VM 在这里赋值才有效,在onLoad方法里赋值,使用的时候变成了null
    LOGI("JVM Agent_OnLoad: %d ,pid: %d",globalVm,getpid());
    LOGI("JVM Agent_OnAttach: %d ,pid: %d",vm,getpid());
    ::globalVm=vm;

    JNIEnv *env;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    LOGI("Find helper class on onattch%s",JVM_TI_CLASS);
    LOGI("Classs Exist:%d", helperClass);

//    ::helperClass = env->FindClass(JVM_TI_CLASS);
    //================================================

    jvmtiEnv *jvmti_env = CreateJvmtiEnv(vm);

    if (jvmti_env == nullptr) {
        return JNI_ERR;
    }
    localJvmtiEnv = jvmti_env;
    SetAllCapabilities(jvmti_env);

    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    
    //设置回调函数
    callbacks.VMObjectAlloc = &onObjectAllocCallback;//绑定内存分配
    callbacks.NativeMethodBind = &JvmTINativeMethodBind;//

    callbacks.GarbageCollectionStart = &onGCStartCallback;//GC 开始
    callbacks.GarbageCollectionFinish = &onGCFinishCallback; //GC 结束

    callbacks.MethodEntry=&onMethodEntry;
    callbacks.MethodExit=&onMethodExit;

    callbacks.ThreadStart=&onThreadStart;
    callbacks.ThreadEnd=&onThreadEnd;


    int error = jvmti_env->SetEventCallbacks(&callbacks, sizeof(callbacks));

    //启用各种回调事件,否则可能不会触发回调方法
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_GARBAGE_COLLECTION_START);//监听GC 开始
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_GARBAGE_COLLECTION_FINISH);//监听GC 结束
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_NATIVE_METHOD_BIND);//监听native method bind
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_VM_OBJECT_ALLOC);//监听对象分配
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_OBJECT_FREE);//监听对象释放
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_CLASS_FILE_LOAD_HOOK);//监听类文件加载
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY);//方法进入
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT);//方法退出

    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_THREAD_START);//线程开始
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_THREAD_END);//线程结束

    LOGI("==========Agent_OnAttach=======");
    return JNI_OK;

}

完全なソースコードを最後に載せておきます

コールバック通知を取得するには、まず次の 2 点が保証されている必要があることに注意してください。

  1. コールバックメソッドを設定する
  2. 対応する監視対象を有効にする

上記の 4 つの要件を達成する方法を見てみましょう

使用方法 1. メモリ割り当てを記録する

メモリ割り当てコールバックをバインドすると、メモリ作成の大部分がコールバック メソッドに通知され、そのほとんどがシステム オブジェクト作成通知になります。ここで独自の新しいメモリ使用量をカウントしたい場合は、不可能ではありませんが、頻繁に通知するとパフォーマンスの消費が高くなります。

  开启 JVMTI_EVENT_GARBAGE_COLLECTION_START 的监听  

 设置回调函数
  callbacks.VMObjectAlloc = &onObjectAllocCallback;//绑定内存分配

このようにして、すべてのメモリ割り当てが onObjectAllocCallback 関数に通知されます。

使用方法 2. GC イベントを記録する

JVMTI_EVENT_GARBAGE_COLLECTION_START と JVMTI_EVENT_GARBAGE_COLLECTION_FINISH は GC 開始と GC 終了のコールバック イベントです。この 2 つのイベントを組み合わせるとメモリ リークにはまだ役立ちますし、この 2 つのイベントの頻度は高くありません。まだ実用的な用途があると思います。


    开启JVMTI_EVENT_GARBAGE_COLLECTION_START和JVMTI_EVENT_GARBAGE_COLLECTION_FINISH监听

    设置回调函数
    
    callbacks.GarbageCollectionStart = &onGCStartCallback;//GC 开始
    callbacks.GarbageCollectionFinish = &onGCFinishCallback; //GC 结束

これら 2 つのイベントが登録されると、onGCStartCallback と onGCFinishCallback の 2 つのメソッドで GC コールバックを受け取ることができます。

使用 3. メソッド呼び出しのログを記録する

jvmtiEventCallbacks のコールバック メソッドを設定し、対応する監視を有効にすると、イベントが発生すると、jvm コールバック イベントが受信されます。
たとえば、MethodEntry と MethodExit の監視を設定した場合、プログラムが開始されると、大量のコールバック メッセージを受信することになります。

    开启JVMTI_EVENT_METHOD_ENTRY 和 JVMTI_EVENT_METHOD_EXIT监听
    
    设置回调函数
    
    callbacks.MethodEntry=&onMethodEntry;
    callbacks.MethodExit=&onMethodExit;

onMethodEntry メソッドでメソッド実行の開始イベントを取得し、onMethodExit でメソッド実行の終了イベントを取得してメソッドの実行時間を計算することもできますが、このメソッドは実際には役に立ちません。jvmti がアプリケーション層と jvm の間に立って、すべての jvm アクティビティがコールバック メソッドに通知され、そのほとんどがフレームワーク層のメソッドであるため、非効率すぎてメソッド呼び出しの監視には適していません。

使用方法 4. スレッドの開始と終了を記録する

スレッドの作成は比較的負荷が低いため、この方法でスレッドの使用量をカウントするのが比較的合理的です。


    开启JVMTI_EVENT_THREAD_START和JVMTI_EVENT_THREAD_END 监听
    
    设置回调函数
    
    callbacks.ThreadStart=&onThreadStart;
    callbacks.ThreadEnd=&onThreadEnd;

2 つのカスタム コールバック メソッド onThreadStart と onThreadEnd でスレッドの作成と終了にこだわることができます。

6. 2 つの興味深い結論は何ですか?

1. わずか 2 つのボタン ページで、アプリの開始から最初のアクティビティが開くまでに 110,000 ものメソッドが実行されます。

このデモでは、プログラマが定義したメソッドが 10 個を超えて実行されることはないため、この比率は非常に法外であり、この方法でメソッドの実行時間を計算することは意味がありません。

%}$08KF1ZE)9D8S84S{)9TB.png

2.アプリの起動から簡単なアクティビティページを開くまでに、割り当てる必要があるオブジェクトの数は約7500です

この単純なデモでは、プログラマー自身の新しいオブジェクトは 10 個を超えません。

画像.png

もちろん、上記の統計は非常に大まかで、標準的な知識ではなく、複雑なデータです...

7. 私が遭遇した問題 (あるいは私の問題かもしれません)

  • 1.1 プログラマのカスタム クラスをコールバック メソッドにロードできない

プログラマがカスタマイズしたクラスを読み込めず、当然カスタムJava層のメソッドも呼び出せない コールバックメソッドでJava層に通知したいのですが、残念ながらカスタムクラスを読み込めず、フレームワークとjavaselibクラスしか読み込めないので(jvmtiには制限があるそうです)、より良い統計を取得するにはCコードをもっと書く必要があります。

  • 1.2 MethodEntryメソッド実行時にメソッドのクラス名を出力したいのですが、ClassのgetNameメソッドを呼び出す必要があるのですが、このメソッドがJava層にあるためデッド再帰が発生してしまうため、メソッド名で除外したいのですが、残念ながら実行効果は確認できているようで、1ステップでデバッグに入ることができません。

  • 1.3 デバッグがうまくいかないことが多い(ちょっと味気ない)

  • 1.4 クラスと javavm のグローバル変数を保存できない (これは私の問題かもしれません)

  • 1.5はデバッグ段階でのみ使用できます(ただし、ボスのハック後のリリース段階でも使用できるようです。正直、これをリリース段階で使用するのは賢明ではありません)

8. 完全なソースコード

https://github.com/woshiwzy/MyAndroidJVMTIDEmo

JVMTI は強力ですが、私が使用しているのは氷山の一角だけです。以下は私が参照した主要な記事です。

9. 参考文献

https://blog.csdn.net/duqi_2009/article/details/94518203

https://blog.csdn.net/z1032689332/article/details/104477182?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.pc_relevant_default& Depth_1-utm _source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.pc_relevant_default&utm_relevant_index=5

https://blog.csdn.net/zhuoxiuwu/article/details/118694396?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6.pc_relevant_paycolumn_v3& Depth _1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6.pc_relevant_paycolumn_v3&utm_relevant_index=9

おすすめ

転載: blog.csdn.net/wang382758656/article/details/124577208