JVMTI使用 - 自定义Agent 监控 Java 程序运行状态

现在我们通过 jvmti 编写自定义的 agent,实现无代码侵入监控 java程序运行状态。

JVMTI官方文档JVM(TM) Tool Interface 1.2.3 (oracle.com)

The JVM tool interface (JVM TI) is a native programming interface for use by tools. It provides both a way to inspect the state and to control the execution of applications running in the Java virtual machine (JVM). JVM TI supports the full breadth of tools that need access to JVM state, including but not limited to: profiling, debugging, monitoring, thread analysis, and coverage analysis tools.

JVM工具接口(JVM TI)是一个为自定义工具提供的本地编程接口。它提供了一种对 JVM 中运行的应用进行检查状态、控制程序执行的方法。JVM TI支持访问JVM状态的全部工具,包括但不限于:剖析、调试、监控、线程分析和覆盖率分析工具。

准备工作

Agent 使用 c/c+开发,生成一个动态链接库,在启动 java 程序作为启动参数进行配置。为了方便管理 Agent 程序采用了 cmake工具。

不同的操作系统对应的动态链接库后缀名有所差异:windows对应 dll,macos 对应dylib

开发环境:

  • MacOS Monterey
  • openjdk version 11.0.11.hs
  • clang version 13.0.0
  • cmake version 3.22.1
  • visual studio code 1.63.2
  • Idea Community 2021.3

1、创建 agent 工程


mkdir agent && cd agent

## 将 JDK 目录下的 include 复制到当前项目下,开发 jvmti 需要引用相关的头文件
cp -r $JAVA_HOME/include ./
## 编写 CmakeList.txt 声明文件
vim CmakeLists.txt
## 编写 agent 代码
vim agent.h
vim agent.cpp

复制代码

项目结构如下:

tree ./

./
├── CmakeLists.txt
├── agent.cpp
├── agent.h
└── include
    ├── classfile_constants.h
    ├── darwin
    │   ├── jawt_md.h
    │   └── jni_md.h
    ├── jawt.h
    ├── jdwpTransport.h
    ├── jni.h
    ├── jvmti.h
    └── jvmticmlr.h

2 directories, 11 files
复制代码

Agent 工程代码(C++)

CmakeLists.txt

## 编译当前项目依赖的 cmake 最低版本
cmake_minimum_required(VERSION 3.0.0)
## 项目名
project(agent)
## 添加依赖目录(之前复制到项目目录下的 include 及其子目录)
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/include/darwin)

## 声明要构建为动态链接库:agent.cpp是我们编写的 agent 主程序,SHARED 表示动态链接库
add_library(agent SHARED agent.cpp)
复制代码

agent.h

jvmti 定义的回调方法列表:docs.oracle.com/javase/8/do…

jvmti 支持的方法列表:docs.oracle.com/javase/8/do…

#ifndef CUSTOMER_AGENT_H
#define CUSTOMER_AGENT_H
#include "jvmti.h"

// JVM 通过回调该方法启动 Agent
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);

// 进入方法是时的回调方法
static void JNICALL
MethodEntry(jvmtiEnv *jvmti_env,
            JNIEnv *jni_env,
            jthread thread,
            jmethodID method);

// 退出方法时的回调方法
static void JNICALL
MethodExit(jvmtiEnv *jvmti_env,
           JNIEnv *jni_env,
           jthread thread,
           jmethodID method,
           jboolean was_popped_by_exception,
           jvalue return_value);

// 退出虚拟机时的回调方法
static void JNICALL
VMDeath(jvmtiEnv *jvmti_env,
        JNIEnv *jni_env);

#endif
复制代码

agent.cpp

#include <iostream>
#include "agent.h"

using namespace std;

// 需要过滤的包名
static char *filter_package = NULL;

// JVM 通过回调该方法启动 Agent
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
    fprintf(stdout, "开始加载自定义 agent\n");
    // 将 option 参数设为要过滤的包名
    filter_package = options;

    jvmtiEnv *jvmti_env = NULL;

    jvm->GetEnv((void **)(&jvmti_env), JVMTI_VERSION_1_1);
    // 配置该 jvmti 要开启的功能
    jvmtiCapabilities caps;
    // 分配内存
    memset(&caps, 0, sizeof(caps));
    // 生成方法进入退出事件
    caps.can_generate_method_entry_events = 1;
    caps.can_generate_method_exit_events = 1;

    // 启用 jvmti 功能
    jvmti_env->AddCapabilities(&caps);

    // 定义回调方法, 配置事件对应的回调方法
    jvmtiEventCallbacks callBacks;
    memset(&callBacks, 0, sizeof(callBacks));
    callBacks.VMDeath = &VMDeath;
    callBacks.MethodEntry = &MethodEntry;
    callBacks.MethodExit = &MethodExit;

    // 设置 jvmti 事件回调
    jvmti_env->SetEventCallbacks(&callBacks, sizeof(callBacks));

    // JVM 退出事件监听
    jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_DEATH, NULL);
    jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
    jvmti_env->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, NULL);

    fprintf(stdout, "成功加载自定义 agent\n");
    return JNI_OK;
}

// 进入方法是时的回调方法
static void JNICALL
MethodEntry(jvmtiEnv *jvmti_env,
            JNIEnv *jni_env,
            jthread thread,
            jmethodID method)
{
    // 获取执行类
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(method, &clazz);
    // 获执行类的签名
    char *class_signature_ptr;
    jvmti_env->GetClassSignature(clazz, &class_signature_ptr, NULL);
    //过滤非本工程类信息
    string::size_type idx;
    // 关键字查找,过滤包名,只打印指定包名下的方法信息
    idx = (string(class_signature_ptr)).find(filter_package);
    if (idx != 1)
    {
        return;
    }

    char *method_name_ptr;
    char *method_signaturn_ptr;
    jvmti_env->GetMethodName(method, &method_name_ptr, &method_signaturn_ptr, NULL);
    fprintf(stdout, "进入方法: %s -> %s\n", method_name_ptr, method_signaturn_ptr);
}

// 退出方法时的回调方法
static void JNICALL
MethodExit(jvmtiEnv *jvmti_env,
           JNIEnv *jni_env,
           jthread thread,
           jmethodID method,
           jboolean was_popped_by_exception,
           jvalue return_value)
{
    // 获取执行类
    jclass clazz;
    jvmti_env->GetMethodDeclaringClass(method, &clazz);
    // 获执行类的签名
    char *class_signature_ptr;
    jvmti_env->GetClassSignature(clazz, &class_signature_ptr, NULL);
    //过滤非本工程类信息
    string::size_type idx;
    // 关键字查找,过滤包名,只打印指定包名下的方法信息
    idx = (string(class_signature_ptr)).find(filter_package);
    if (idx != 1)
    {
        return;
    }
    char *method_name_ptr;
    char *method_signaturn_ptr;
    jvmti_env->GetMethodName(method, &method_name_ptr, &method_signaturn_ptr, NULL);
    fprintf(stdout, "退出方法: %s -> %s\n", method_name_ptr, method_signaturn_ptr);
}

// 退出虚拟机时的回调方法
static void JNICALL
VMDeath(jvmtiEnv *jvmti_env,
        JNIEnv *jni_env)
{

    fprintf(stdout, "退出虚拟机\n");
}
复制代码

编译打包 agent 动态链接库

## 在 agent 项目中创建 build 目录,用于统一管理编译打包过程中生成的所有文件
mkdir build && cd build

## 执行编译打包
cmake .. && make

## 查看当前的项目结构
tree ./
./
├── CMakeCache.txt
├── CMakeFiles # Cmake 生成的中间文件,无需关注
│   │—— ......   
├── Makefile # make 命令所需的声明文件
├── cmake_install.cmake
├── compile_commands.json
└── libagent.dylib # 最终生成的动态链接库

8 directories, 33 files


复制代码

Java 配置自定义 Agent

此时我们已经有了agent 对应的动态链接库,只需要在 java 启动时将该 agent 的添加即可。

HelloAgent.java

package org.agent.demo;

/**
 * @author ZhangYaqi
 * @version 0.1.0
 * @date 2021-12-10
 */
public class HelloAgent {
    public static void main(String[] args)  {
        System.out.println("Java 程序启动");
        greetAgent("Agent");
    }


    static void greetAgent(String msg) {
        System.out.printf("Hello, %s\n", msg);
    }
}

复制代码

IDEA配置启动参数

配置 agent 启动参数格式如下:

-agentpath:<pathname>=<options>

pathname为我们之前生成的动态链接库的地址

options 为agent 启动时传入的配置参数,这里我们用来传递 filter_package信息

配置如下:

Edite Configurations → VM options 添加如下信息,如果找不到 VM options,可以在Modify options中找到 add VM options 选项

-agentpath:/coding/agent/build/libagent.dylib=org/agent/demo

运行 HelloAgent 的 main方法,就可以看到我们在自定义 agent中添加的打印信息:

Java 程序启动
Hello, Agent
开始加载自定义 agent
成功加载自定义 agent
进入方法: main -> ([Ljava/lang/String;)V
进入方法: greetAgent -> (Ljava/lang/String;)V
退出方法: greetAgent -> (Ljava/lang/String;)V
退出方法: main -> ([Ljava/lang/String;)V
退出虚拟机
复制代码

Guess you like

Origin juejin.im/post/7047381967058239525