基於jvmti定位java異常信息

微信公衆號:bugstack蟲洞棧html

沉澱、分享、成長,專一於原創專題案例,以最易學習編程的方式分享知識,讓本身和他人都能有所收穫。目前已完成的專題有;Netty4.x實戰專題案例、用Java實現JVM、基於JavaAgent的全鏈路監控、手寫RPC框架、架構設計專題案例[Ing]等。前端

背景描述java

JVMTI(JVM Tool Interface)位於jpda最底層,是Java虛擬機所提供的native編程接口。JVMTI能夠提供性能分析、debug、內存管理、線程分析等功能。ios

JPDA 定義了一個完整獨立的體系,它由三個相對獨立的層次共同組成,並且規定了它們三者之間的交互方式,或者說定義了它們通訊的接口。這三個層次由低到高分別是 Java 虛擬機工具接口(JVMTI),Java 調試線協議(JDWP)以及 Java 調試接口(JDI)。這三個模塊把調試過程分解成幾個很天然的概念:調試者(debugger)和被調試者(debuggee),以及他們中間的通訊器。被調試者運行於咱們想調試的 Java 虛擬機之上,它能夠經過 JVMTI 這個標準接口,監控當前虛擬機的信息;調試者定義了用戶可以使用的調試接口,經過這些接口,用戶能夠對被調試虛擬機發送調試命令,同時調試者接受並顯示調試結果。在調試者和被調試着之間,調試命令和調試結果,都是經過 JDWP 的通信協議傳輸的。全部的命令被封裝成 JDWP 命令包,經過傳輸層發送給被調試者,被調試者接收到 JDWP 命令包後,解析這個命令並轉化爲 JVMTI 的調用,在被調試者上運行。相似的,JVMTI 的運行結果,被格式化成 JDWP 數據包,發送給調試者並返回給 JDI 調用。而調試器開發人員就是經過 JDI 獲得數據,發出指令。 c++

JDPA 模塊層次.png

模塊 層次 編程語言 做用
JVMTI 底層 C 獲取及控制當前虛擬機狀態
JDWP 中介層 C 定義 JVMTI 和 JDI 交互的數據格式
JDI 高層 Java 提供 Java API 來遠程控制被調試虛擬機

Java 虛擬機工具接口(JVMTI) JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機工具接口,它是一套由虛擬機直接提供的 native 接口,它處於整個 JPDA 體系的最底層,全部調試功能本質上都須要經過 JVMTI 來提供。經過這些接口,開發人員不只調試在該虛擬機上運行的 Java 程序,還能查看它們運行的狀態,設置回調函數,控制某些環境變量,從而優化程序性能。咱們知道,JVMTI 的前身是 JVMDI 和 JVMPI,它們原來分別被用於提供調試 Java 程序以及 Java 程序調節性能的功能。在 J2SE 5.0 以後 JDK 取代了 JVMDI 和 JVMPI 這兩套接口,JVMDI 在最新的 Java SE 6 中已經不提供支持,而 JVMPI 也計劃在 Java SE 7 後被完全取代。編程

Java 調試線協議(JDWP) JDWP(Java Debug Wire Protocol)是一個爲 Java 調試而設計的一個通信交互協議,它定義了調試器和被調試程序之間傳遞的信息的格式。在 JPDA 體系中,做爲前端(front-end)的調試者(debugger)進程和後端(back-end)的被調試程序(debuggee)進程之間的交互數據的格式就是由 JDWP 來描述的,它詳細完整地定義了請求命令、迴應數據和錯誤代碼,保證了前端和後端的 JVMTI 和 JDI 的通訊通暢。好比在 Sun 公司提供的實現中,它提供了一個名爲 jdwp.dll(jdwp.so)的動態連接庫文件,這個動態庫文件實現了一個 Agent,它會負責解析前端發出的請求或者命令,並將其轉化爲 JVMTI 調用,而後將 JVMTI 函數的返回值封裝成 JDWP 數據發還給後端。後端

Java 調試接口(JDI) JDI(Java Debug Interface)是三個模塊中最高層的接口,在多數的 JDK 中,它是由 Java 語言實現的。 JDI 由針對前端定義的接口組成,經過它,調試工具開發人員就能經過前端虛擬機上的調試器來遠程操控後端虛擬機上被調試程序的運行,JDI 不只能幫助開發人員格式化 JDWP 數據,並且還能爲 JDWP 數據傳輸提供隊列、緩存等優化服務。從理論上說,開發人員只需使用 JDWP 和 JVMTI 便可支持跨平臺的遠程調試,可是直接編寫 JDWP 程序費時費力,並且效率不高。所以基於 Java 的 JDI 層的引入,簡化了操做,提升了開發人員開發調試程序的效率。api

開發簡述緩存

基於jvmti提供的接口服務,運用C++代碼(win32-add_library)在Agent_OnLoad裏開發監控服務,並生成dll文件。開發完成後在java代碼中加入agentpath,這樣就能夠監控到咱們須要的信息內容。bash

環境準備

  1. Dev-C++
  2. JetBrains CLion 2018.2.3
  3. IntelliJ IDEA Community Edition 2018.3.1 x64
  4. jdk1.8 64位
  5. jvmti(在jdk安裝目錄下jdk1.8.0_45\include裏,複製到和工程案例同層級目錄下)

配置信息(路徑相關修改成本身的)

  1. C++開發工具Clion配置
    1. 配置位置;Settings->Build,Execution,Deployment->Toolchains
    2. MinGM配置:D:\Program Files (x86)\Dev-Cpp\MinGW64
  2. java調試時配置
    1. 配置位置:Run/Debug Configurations ->VM options
    2. 配置內容:-agentpath:E:\itstack\itstack.org\demo\jvmti\itstack-demo-jvmti-dll\cmake-build-debug\libitstack_demo_jvmti_dll.dll

代碼示例

c++ 代碼塊:

#include <iostream>
#include <cstring>
#include "jvmti.h"

using namespace std;

//異常回調函數
static void JNICALL callbackException(jvmtiEnv *jvmti_env, JNIEnv *env, jthread thr, jmethodID methodId, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location) {

    // 得到方法對應的類
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(methodId, &clazz);

    // 得到類的簽名
    char *class_signature;
    jvmti_env->GetClassSignature(clazz, &class_signature, nullptr);

    //過濾非本工程類信息
    string::size_type idx;
    string class_signature_str = class_signature;
    idx = class_signature_str.find("org/itstack");
    if (idx != 1) {
        return;
    }

    //異常類名稱
    char *exception_class_name;
    jclass exception_class = env->GetObjectClass(exception);
    jvmti_env->GetClassSignature(exception_class, &exception_class_name, nullptr);

    // 得到方法名稱
    char *method_name_ptr, *method_signature_ptr;
    jvmti_env->GetMethodName(methodId, &method_name_ptr, &method_signature_ptr, nullptr);

    //獲取目標方法的起止地址和結束地址
    jlocation start_location_ptr;    //方法的起始位置
    jlocation end_location_ptr;      //用於方法的結束位置
    jvmti_env->GetMethodLocation(methodId, &start_location_ptr, &end_location_ptr);

    //輸出測試結果
    cout << "測試結果-定位類的簽名:" << class_signature << endl;
    cout << "測試結果-定位方法信息:" << method_name_ptr << " -> " << method_signature_ptr << endl;
    cout << "測試結果-定位方法位置:" << start_location_ptr << " -> " << end_location_ptr + 1 << endl;
    cout << "測試結果-異常類的名稱:" << exception_class_name << endl;

    cout << "測試結果-輸出異常信息(能夠分析行號):" << endl;
    jclass throwable_class = (*env).FindClass("java/lang/Throwable");
    jmethodID print_method = (*env).GetMethodID(throwable_class, "printStackTrace", "()V");
    (*env).CallVoidMethod(exception, print_method);

}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *gb_jvmti = nullptr;
    //初始化
    vm->GetEnv(reinterpret_cast<void **>(&gb_jvmti), JVMTI_VERSION_1_0);
    // 建立一個新的環境
    jvmtiCapabilities caps;
    memset(&caps, 0, sizeof(caps));
    caps.can_signal_thread = 1;
    caps.can_get_owned_monitor_info = 1;
    caps.can_generate_method_entry_events = 1;
    caps.can_generate_exception_events = 1;
    caps.can_generate_vm_object_alloc_events = 1;
    caps.can_tag_objects = 1;
    // 設置當前環境
    gb_jvmti->AddCapabilities(&caps);
    // 建立一個新的回調函數
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    //異常回調
    callbacks.Exception = &callbackException;
    // 設置回調函數
    gb_jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    // 開啓事件監聽(JVMTI_EVENT_EXCEPTION)
    gb_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_EXCEPTION, nullptr);
    return JNI_OK;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
}

複製代碼

java代碼塊:

package org.itstack.demo.jvmti;
import java.util.logging.Logger;

public class TestLocationException {

    public static void main(String[] args) {
        Logger logger = Logger.getLogger("TestLocationException");
        try {
            User resource = new User();
            Object obj = resource.queryUserInfoById(null);
            logger.info("測試結果:" + obj);
        } catch (Exception e) {
            //屏蔽異常
        }
    }
}

class User {
    Logger logger = Logger.getLogger("User");
    public Object queryUserInfoById(String userId) {
        logger.info("根據用戶Id獲取用戶信息" + userId);
        if (null == userId) {
            throw new NullPointerException("根據用戶Id獲取用戶信息,空指針異常");
        }
        return userId;
    }
}
複製代碼

測試結果

四月 13, 2019 12:21:45 下午 org.itstack.demo.jvmti.User queryUserInfoById
信息: 根據用戶Id獲取用戶信息null
測試結果-定位類的簽名:Lorg/itstack/demo/jvmti/User;
測試結果-定位方法信息:queryUserInfoById -> (Ljava/lang/String;)Ljava/lang/Object;
測試結果-定位方法位置:0 -> 43
測試結果-異常類的名稱:Ljava/lang/NullPointerException;
測試結果-輸出異常信息(能夠分析行號):
java.lang.NullPointerException: 根據用戶Id獲取用戶信息,空指針異常
	at org.itstack.demo.jvmti.User.queryUserInfoById(TestLocationException.java:23)
	at org.itstack.demo.jvmti.TestLocationException.main(TestLocationException.java:10)
複製代碼

其餘內容:

  1. jvmti api
  2. JPDA 體系概覽

微信公衆號:bugstack蟲洞棧,歡迎關注&獲取源碼
相關文章
相關標籤/搜索