Java-Agent

简介

Java Agent 直译过来叫做 Java 代理,但更多称叫做 Java 探针

Java Agent是一种特殊的Java程序(Jar文件),与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API与虚拟机交互

相关概念

Instrumentation

Instrumentation是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoaderclasspath下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)

主流的JVM都提供了Instrumentation的实现,但是鉴于Instrumentation的特殊功能,并不适合直接提供在JDK的runtime里,而更适合出现在Java程序的外层,以上帝视角在合适的时机出现。因此如果想使用Instrumentation功能,「拿到Instrumentation实例,我们必须通过Java agent」Instrumentation常用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface Instrumentation {

//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,
//如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。
//对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);

//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);

//是否允许对class retransform
boolean isRetransformClassesSupported();

//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

//是否允许对class重新定义
boolean isRedefineClassesSupported();

//此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译以进行修复和继续调试时所做的那样。
//在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。
//该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;

//获取已经被JVM加载的class,有className可能重复(可能存在多个classloader)
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
}

其中最常用的方法就是addTransformer(ClassFileTransformer transformer)了,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口,定义如下:

1
2
3
4
5
/**
* 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
* 返回值为需要被修改后的字节码byte[]
*/
byte[] transform( ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;

addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复

Attach

Attach API其实是跨JVM进程通讯的工具,能够将某种指令从一个JVM进程发送给另一个JVM进程

Attach机制可以对目标进程收集很多信息,如内存dump,线程dump,类信息统计(比如加载的类及大小以及实例个数等),动态加载agent,动态设置vm flag,打印vm flag,获取系统属性等等

Java Agent结构

Java Agent 最终以 jar 包的形式存在。主要包含两个部分,一部分是实现代码,一部分是配置文件。配置文件放在 META-INF 目录下,文件名为 MANIFEST.MF

代码入口是premainagentmain方法,具体选用哪个方法以及其中的内容根据应用场景决定

配置文件参数说明:

  • Manifest-Version: 版本号
  • Created-By: 创作者
  • Agent-Class: agentmain方法所在类
  • Can-Redefine-Classes: 是否可以实现类的重定义
  • Can-Retransform-Classes: 是否可以实现字节码替换
  • Premain-Class: premain 方法所在类

使用场景

Java Agent 技术有以下主要功能:

  • 在加载Java文件前拦截字节码并做修改
  • 在运行期间变更已加载的类的字节码
  • 获取所有已经被加载过的类
  • 获取所有已经被初始化过了的类
  • 获取某个对象的大小

基于这些功能,衍生出了很多常见工具,Java调式、热部署、线上诊断等工具都有依赖Java Agent:

  • 各个 Java IDE 的调试功能,例如 eclipse、IntelliJ

  • 热部署功能,例如 JRebel、XRebel、spring-loaded

  • 各种线上诊断工具,例如 Btrace、Greys,还有阿里的 Arthas

  • 各种性能分析工具,例如 Visual VM、JConsole 等

使用方法

Java Agent分为两种:静态Agent与动态Agent

image-20221008202408373

静态Agent

这种方式是使用premain作为Agent的入口方法,以JVM启动参数-javaagent:xxx.jar方式载入,在Java程序的main方法执行之前执行

  1. 编写premain方法,应该包含以下两个方法中的一个:

    1
    2
    public static void premain(String args, Instrumentation inst);
    public static void premain(String args);

    JVM 会优先加载带Instrumentation签名的方法1,加载成功忽略方法2,如果没有Instrumentation签名的方法,则加载方法2

  2. 定义一个MANIFEST.MF文件,其中必须包含Premain-Class选项

  3. 将包含premain的类与MANIFEST.MF配置文件打包成一个 jar 包

  4. 在JVM启动参数中添加-javaagent:[path],其中的path为对应的Agent的jar包路径。这样则将Agent挂载成功,Java程序再执行main方法前执行

动态Agent

与静态方式不同,动态Agent允许代理的目标程序的JVM先启动,再通过attach机制载入

  1. 同样需要实现agentmain方法,加载优先级与premain相同,带Instrumentation签名的方法优先

    1
    2
    public static void agentmain(String args, Instrumentation inst);
    public static void agentmain(String args);
  2. 定义一个MANIFEST.MF文件,与静态Agent不同的是,此时必须包含Agent-Class选项

  3. 同样将包含agentmain的类与MANIFEST.MF配置文件打包成一个 jar 包

  4. premain模式不同,不再通过添加启动参数的方式来连接agent和主程序了,而使用attach方式来挂载。attach方式使用了com.sun.tools.attach包下的VirtualMachine工具类

    1
    2
    3
    4
    5
    6
    // 获取所有VM实例
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    // attach对应VM
    VirtualMachine attach = VirtualMachine.attach(descriptor);
    // 加载目标Agent
    attach.loadAgent("Java-Agent路径");

Demo

最常用InstrumentationaddTransformer方法对类加载做拦截,对输入的类的字节码进行修改、增强。依赖字节码修改,字节码修改技术主要有 Javassist、ASM,Javassist使用更简单,这里使用Javassist来进行字节码修改,引入相关maven包

1
2
3
4
5
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>

Javassist 使用可以参考下面文档:

基于 Javassist 和 Javaagent 实现动态切面

Javassist API

实现极简的watch命令

模拟Arthas的watch命令,来统计方法执行耗时

开发Agent

https://github.com/ShadowTwj/sandbox/tree/master/agent/src/main/java/cn/tianwenjie/watch

代码

  1. Agent入口方法,包括premainagentmain两个方法,后面会分别测试两个场景
image-20221010202535725
  1. 实现类转换器,来对指定类和方法增强

    image-20221010202839064

配置文件

配置MANIFEST.MF文件,指定Premain-ClassAgent-Class等属性,将配置文件与代码一同打包生成jar包

也可以使用maven的maven-assembly-plugin插件,来进行打包,参数可直接配置在pom文件中,打包的时候就会自动将配置信息生成 MANIFEST.MF 配置文件打进包里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Built-By>neo</Built-By>
<Premain-Class>cn.tianwenjie.watch.WatchDemo</Premain-Class>
<Agent-Class>cn.tianwenjie.watch.WatchDemo</Agent-Class>
<Main-Class>cn.tianwenjie.watch.WatchDemo</Main-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>

测试

分别测试Agent的两种方式

https://github.com/ShadowTwj/sandbox/tree/master/agent-run/src/main/java/cn/tianwenjie/watch

静态Agent测试

  • 打印控制台输入,统计打印方法耗时
image-20221011110030840
  • 添加VM options,指定Agent Jar包路径
image-20221011110416104
  • 执行main方法,观察print方法耗时

    image-20221011110730915

动态Agent测试

  • 测试方法如上,打印控制台输入,统计打印方法耗时

  • 不需要添加VM options,直接执行main方法,观察未挂载Agent前控制台只打印了输入参数,没有打印方法耗时,该方法不要结束,等待下面Attach

    image-20221011111320354
  • Attach VM,挂载Agent Jar包

    image-20221011111737935
  • Attach成功后,再执行测试类,观察到方法增强成功,打印控制台输入的同时打印方法耗时

    image-20221011112040162

模拟热加载

模拟热加载,重新加载修改的类

与watch命令最大的区别是没有使用字节码修改技术,而是自定义编译器,将新的代码编译为字节码

开发Agent

https://github.com/ShadowTwj/sandbox/tree/master/agent/src/main/java/cn/tianwenjie/hotdeploy

代码

  1. Agent入口方法,静态Agent对热加载无用,这里只实现动态Agent的agentmain方法

    使用retransformredefineClasses方法效果一样,这里使用redefineClasses方法,不在实现类转换器

    image-20221112180722715
  2. 自定义编译器,因为热加载改动代码大多都不可预测,使用字节码修改技术并不方便,这里自定义编译器,来编译成字节码

    具体代码:

    https://github.com/ShadowTwj/sandbox/blob/master/agent/src/main/java/cn/tianwenjie/hotdeploy/common/DynamicCompiler.java

配置文件

修改maven-assembly-plugin插件配置,打包生成配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Built-By>neo</Built-By>
<Premain-Class>cn.tianwenjie.hotdeploy.HotLoadingDemo</Premain-Class>
<Agent-Class>cn.tianwenjie.hotdeploy.HotLoadingDemo</Agent-Class>
<Main-Class>cn.tianwenjie.hotdeploy.HotLoadingDemo</Main-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>

测试

https://github.com/ShadowTwj/sandbox/tree/master/agent-run/src/main/java/cn/tianwenjie/hotdeploy

  • 测试方法同上watch命令,打印控制台输入

    image-20221112182332843
  • Attach VM,挂载Agent Jar包,热加载新类。如下图,继续打印控制台输入,可以看到热加载成功

    image-20221112182533197

Java-Agent原理

静态Agent

启动时加载过程

JPLISAgent:作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任

  1. 创建并初始化 JPLISAgent;
  2. 监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
    1. 创建 InstrumentationImpl 对象 ;
    2. 监听 ClassFileLoadHook 事件 ;
    3. 调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法 ;
  3. 解析 javaagent 中 MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容。

源码

  1. 参数解析

    JVM启动时解析对应参数,观察hotspot/src/share/vm/runtime/arguments.cpp中的Arguments::parse_each_vm_init_arg函数片段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // -agentlib and -agentpath
    if (match_option(option, "-agentlib:", &tail) ||
    (is_absolute_path = match_option(option, "-agentpath:", &tail))) {
    if(tail != NULL) {
    const char* pos = strchr(tail, '=');
    char* name;
    if (pos == NULL) {
    name = os::strdup_check_oom(tail, mtArguments);
    } else {
    size_t len = pos - tail;
    name = NEW_C_HEAP_ARRAY(char, len + 1, mtArguments);
    memcpy(name, tail, len);
    name[len] = '\0';
    }

    char *options = NULL;
    if(pos != NULL) {
    options = os::strdup_check_oom(pos + 1, mtArguments);
    }
    #if !INCLUDE_JVMTI
    if (valid_jdwp_agent(name, is_absolute_path)) {
    jio_fprintf(defaultStream::error_stream(),
    "Debugging agents are not supported in this VM\n");
    return JNI_ERR;
    }
    #endif // !INCLUDE_JVMTI
    // 存储Agent解析结果
    // name:"instrument",动态链接库
    add_init_agent(name, options, is_absolute_path);
    }
    // -javaagent
    } else if (match_option(option, "-javaagent:", &tail)) {
    #if !INCLUDE_JVMTI
    jio_fprintf(defaultStream::error_stream(),
    "Instrumentation agents are not supported in this VM\n");
    return JNI_ERR;
    #else
    if (tail != NULL) {
    size_t length = strlen(tail) + 1;
    char *options = NEW_C_HEAP_ARRAY(char, length, mtArguments);
    jio_snprintf(options, length, "%s", tail);
    add_instrument_agent("instrument", options, false);
    // java agents need module java.instrument
    if (!create_numbered_module_property("jdk.module.addmods", "java.instrument", addmods_count++)) {
    return JNI_ENOMEM;
    }
    }
    #endif // !INCLUDE_JVMTI

    这段逻辑用来解析需要加载的Agent路径,然后调用add_init_agent存储解析结果到_agentList中,AgentLibraryList是一个链表

    1
    2
    3
    4
    5
    6
    // -agentlib and -agentpath arguments
    static AgentLibraryList _agentList;

    void Arguments::add_init_agent(const char* name, char* options, bool absolute_path) {
    _agentList.add(new AgentLibrary(name, options, absolute_path, NULL));
    }
  2. 加载Agent

    观察hotspot/src/share/vm/runtime/threads.cpp中的Threads::create_vm函数,JVM在解析完参数后,判断_agentList是否为空,不为空加载Agent

    1
    2
    3
    4
    5
    6
    7
    // Launch -agentlib/-agentpath and converted -Xrun agents
    // 判断agent链表是否为空,不为空加载Agent
    if (Arguments::init_agents_at_startup()) {
    create_vm_init_agents();
    }

    static bool init_agents_at_startup() { return !_agentList.is_empty(); }

    分析create_vm_init_agents函数,遍历Agent逐个加载,解析对应的Agent_Onload函数,最终调用premain方法,执行Agent_Onload函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
    // CDS dumping does not support native JVMTI agent.
    // CDS dumping supports Java agent if the AllowArchivingWithJavaAgent diagnostic option is specified.
    if (Arguments::is_dumping_archive()) {
    if(!agent->is_instrument_lib()) {
    vm_exit_during_cds_dumping("CDS dumping does not support native JVMTI agent, name", agent->name());
    } else if (!AllowArchivingWithJavaAgent) {
    vm_exit_during_cds_dumping(
    "Must enable AllowArchivingWithJavaAgent in order to run Java agent during CDS dumping");
    }
    }

    // 解析Agent_OnLoad函数,最终调用premain方法
    OnLoadEntry_t on_load_entry = lookup_agent_on_load(agent);

    if (on_load_entry != NULL) {
    // Invoke the Agent_OnLoad function
    // 执行Agent_OnLoad函数
    jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
    if (err != JNI_OK) {
    vm_exit_during_initialization("agent library failed to init", agent->name());
    }
    } else {
    vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());
    }
    }
  3. Agent_OnLoad函数

    动态链接库libinstrument,用来支持使用Java Instrumentation API来编写Agent,在libinstrument中有一个非常重要的类称为:JPLISAgent(Java Programming Language Instrumentation Services Agent),它的作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任

    在动态链接库libinstrument中找到Agent_OnLoad函数,在java.instrument/share/native/libinstrument/InvocationAdapter.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    JNIEXPORT jint JNICALL
    DEF_Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {
    JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
    jint result = JNI_OK;
    JPLISAgent * agent = NULL;

    // 1. 创建并初始化JPLISAgent
    initerror = createNewJPLISAgent(vm, &agent);
    if ( initerror == JPLIS_INIT_ERROR_NONE ) {
    int oldLen, newLen;
    char * jarfile;
    char * options;
    jarAttribute* attributes;
    char * premainClass;
    char * bootClassPath;

    /*
    * Parse <jarfile>[=options] into jarfile and options
    */
    if (parseArgumentTail(tail, &jarfile, &options) != 0) {
    fprintf(stderr, "-javaagent: memory allocation failure.\n");
    return JNI_ERR;
    }

    attributes = readAttributes(jarfile);

    //...

    premainClass = getAttribute(attributes, "Premain-Class");

    //...

    /* Save the jarfile name */
    agent->mJarfile = jarfile;

    //...

    bootClassPath = getAttribute(attributes, "Boot-Class-Path");

    /*
    * Convert JAR attributes into agent capabilities
    */
    convertCapabilityAttributes(attributes, agent);

    /*
    * Track (record) the agent class name and options data
    */
    initerror = recordCommandLineData(agent, premainClass, options);

    /*
    * Clean-up
    */
    if (options != NULL) free(options);
    freeAttributes(attributes);
    free(premainClass);
    }

    return result;
    }

    可以看到函数中调用createNewJPLISAgent方法,创建JPLISAgent,在createNewJPLISAgent函数中又调用initializeJPLISAgent函数进行初始化,initializeJPLISAgent函数中有设置VMInit时间回调函数

    1
    2
    // 2.触发VMInit事件回调
    callbacks.VMInit = &eventHandlerVMInit;

    看下eventHandlerVMInit函数实现,eventHandlerVMInit -> processJavaStart -> startJavaAgent -> invokeJavaAgentMainMethod,最终invokeJavaAgentMainMethod函数则是调用premain方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    void JNICALL
    eventHandlerVMInit( jvmtiEnv * jvmtienv,
    JNIEnv * jnienv,
    jthread thread) {
    //...
    success = processJavaStart( environment->mAgent, jnienv);
    //...
    }

    jboolean
    processJavaStart( JPLISAgent * agent,
    JNIEnv * jnienv) {
    /*
    * Now make the InstrumentationImpl instance.
    */
    if ( result ) {
    result = createInstrumentationImpl(jnienv, agent);
    jplis_assert_msg(result, "instrumentation instance creation failed");
    }

    /*
    * Load the Java agent, and call the premain.
    */
    if ( result ) {
    result = startJavaAgent(agent, jnienv,
    agent->mAgentClassName, agent->mOptionsString,
    agent->mPremainCaller);
    jplis_assert_msg(result, "agent load/premain call failed");
    }
    }

    jboolean
    startJavaAgent( JPLISAgent * agent,
    JNIEnv * jnienv,
    const char * classname,
    const char * optionsString,
    jmethodID agentMainMethod) {
    //...

    // 3.执行premain方法
    success = invokeJavaAgentMainMethod( jnienv,
    agent->mInstrumentationImpl,
    agentMainMethod,
    classNameObject,
    optionsStringObject);

    return success;
    }

动态Agent

运行时加载过程

通过 JVM 的attach机制来请求目标 JVM 加载对应的agent,过程大致如下:

  1. 创建并初始化JPLISAgent;
  2. 解析 javaagent 里 MANIFEST.MF 里的参数;
  3. 创建 InstrumentationImpl 对象;
  4. 监听 ClassFileLoadHook 事件;
  5. 调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

Attach

动态Agent是通过Attach机制来加载,下面分析下Attach原理

  1. AttachListener

    Attach机制通过Attach Listener线程来进行相关事务的处理,AttachListener初始化如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void AttachListener::init() {
    EXCEPTION_MARK;

    const char* name = "Attach Listener";
    Handle thread_oop = JavaThread::create_system_thread_object(name, true /* visible */, THREAD);
    if (has_init_error(THREAD)) {
    set_state(AL_NOT_INITIALIZED);
    return;
    }

    JavaThread* thread = new JavaThread(&attach_listener_thread_entry);
    JavaThread::vm_exit_on_osthread_failure(thread);

    JavaThread::start_internal_daemon(THREAD, thread, thread_oop, NoPriority);
    }

    attach_listener_thread_entry函数是线程入口,代码片段如下:

    首先获取到Attach任务,然后查询匹配命令对应的函数,最后执行对应函数,funcs是命令对应的函数,其中”load”命令对应load_agent函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
    AttachListener::set_initialized();
    for (;;) {
    // 1.获取Attach任务
    AttachOperation* op = AttachListener::dequeue();

    // find the function to dispatch too
    // 2.查询匹配命令对应的函数,funcs是命令对应的函数,其中"load"命令对应load_agent函数
    AttachOperationFunctionInfo* info = NULL;
    for (int i=0; funcs[i].name != NULL; i++) {
    const char* name = funcs[i].name;
    if (strcmp(op->name(), name) == 0) {
    info = &(funcs[i]); break;
    }}

    // dispatch to the function that implements this operation
    // 3.执行命令对应的函数
    res = (info->func)(op, &st);
    //...
    }
    }

    查看dequeue函数,是如何获取任务的,dequeue函数不系统实现不同,windows系统是Win32AttachListener::dequeue(),Mac系统是BsdAttachListener::dequeue(),Linux系统是LinuxAttachListener::dequeue()。下面是Linux系统实现,等待客户端连接,通过accept来接收,然后将请求读出来,包装成AttachOperation对象,返回进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    LinuxAttachOperation* LinuxAttachListener::dequeue() {
    for (;;) {
    // wait for client to connect
    struct sockaddr addr;
    socklen_t len = sizeof(addr);
    // 等待连接,通过accept来接收
    RESTARTABLE(::accept(listener(), &addr, &len), s);
    // get the credentials of the peer and check the effective uid/guid
    // - check with jeff on this.
    struct ucred cred_info;
    socklen_t optlen = sizeof(cred_info);
    if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void*)&cred_info, &optlen) == -1) {
    ::close(s);
    continue;
    }

    // peer credential look okay so we read the request
    // 将请求读出来
    LinuxAttachOperation* op = read_request(s);
    return op;
    }
    }
  2. VirtualMachine.attach方法

    通过com.sun.tools.attach.VirtualMachine#attach方法来连接指定pid的JVM进程,查看源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static VirtualMachine attach(String var0) throws AttachNotSupportedException, IOException {
    if (var0 == null) {
    throw new NullPointerException("id cannot be null");
    } else {
    List var1 = AttachProvider.providers();
    if (var1.size() == 0) {
    throw new AttachNotSupportedException("no providers installed");
    } else {
    AttachNotSupportedException var2 = null;
    Iterator var3 = var1.iterator();

    while(var3.hasNext()) {
    AttachProvider var4 = (AttachProvider)var3.next();

    try {
    return var4.attachVirtualMachine(var0);
    } catch (AttachNotSupportedException var6) {
    var2 = var6;
    }
    }

    throw var2;
    }
    }
    }

    可以看出最终调用AttachProviderattachVirtualMachine方法,AttachProvider是抽象类,不同系统不同实现,在MacOS中实现类是BsdAttachProvider,其中attachVirtualMachine实现是:

    1
    2
    3
    4
    5
    public VirtualMachine attachVirtualMachine(String var1) throws AttachNotSupportedException, IOException {
    this.checkAttachPermission();
    this.testAttachable(var1);
    return new BsdVirtualMachine(this, var1);
    }

    查看其构造方法,通过findSocketFile方法用来查询目标JVM上是否已经启动了Attach Listener,因为Attach Listener是懒加载,所以JVM启动也不一定加载。检查/tmp/.java_pid{pid}文件是否存在,如果存在,则说明目标JVM Attach机制已准备就绪,可以直接通过connect方法来连接到目标JVM,发送命令;如果不存在,则说明目标JVM的Attach Listener还没有初始化,这时通过sendQuitTo方法向目标JVM发送信号,让其初始化Attach Listener,并且循环等待/tmp/.java_pid{pid}文件的创建,之后再通过connect方法来连接到目标JVM,发送命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    BsdVirtualMachine(AttachProvider var1, String var2) throws AttachNotSupportedException, IOException {
    super(var1, var2);

    int var3;
    try {
    var3 = Integer.parseInt(var2);
    } catch (NumberFormatException var22) {
    throw new AttachNotSupportedException("Invalid process identifier");
    }

    // var3为pid,检查/tmp/.java_pid{pid}是否存在
    this.path = this.findSocketFile(var3);
    // 如果存在,则说明目标JVM Attach机制已准备就绪,可以直接通过connect方法来连接到目标JVM,发送命令
    // 如果不存在,则说明目标JVM的Attach Listener还没有初始化
    if (this.path == null) {
    // 创建/tmp/.java_pid{pid}文件
    File var4 = new File(tmpdir, ".attach_pid" + var3);
    createAttachFile(var4.getPath());

    try {
    // 向目标JVM发送信号,让其初始化Attach Listener
    sendQuitTo(var3);
    int var5 = 0;
    long var6 = 200L;
    int var8 = (int)(this.attachTimeout() / var6);

    // 循环等待/tmp/.java_pid{pid}文件的创建,之后再通过connect方法来连接到目标JVM,发送命令
    do {
    try {
    Thread.sleep(var6);
    } catch (InterruptedException var21) {
    }

    this.path = this.findSocketFile(var3);
    ++var5;
    } while(var5 <= var8 && this.path == null);

    if (this.path == null) {
    throw new AttachNotSupportedException("Unable to open socket file: target process not responding or HotSpot VM not loaded");
    }
    } finally {
    var4.delete();
    }
    }

    checkPermissions(this.path);
    int var24 = socket();

    try {
    connect(var24, this.path);
    } finally {
    close(var24);
    }

    }
  3. loadAgent方法

    通过attach方法,连接上目标JVM后,通过loadAgent方法来加载Agent,其本质是向目标JVM发送load命令,这里不再展开

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private void loadAgentLibrary(String var1, boolean var2, String var3) throws AgentLoadException, AgentInitializationException, IOException {
    InputStream var4 = this.execute("load", var1, var2 ? "true" : "false", var3);

    try {
    int var5 = this.readInt(var4);
    if (var5 != 0) {
    throw new AgentInitializationException("Agent_OnAttach failed", var5);
    }
    } finally {
    var4.close();
    }

    }

    看下JVM中load命令的实现,上面Agtach Listenerattach_listener_thread_entry函数中,会查询匹配命令对应的函数,然后执行对应的函数,funcs则是一个命令函数表,查看load命令对应的函数,发现是load_agent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // names must be of length <= AttachOperation::name_length_max
    static AttachOperationFunctionInfo funcs[] = {
    { "agentProperties", get_agent_properties },
    { "datadump", data_dump },
    { "dumpheap", dump_heap },
    { "load", load_agent },
    { "properties", get_system_properties },
    { "threaddump", thread_dump },
    { "inspectheap", heap_inspection },
    { "setflag", set_flag },
    { "printflag", print_flag },
    { "jcmd", jcmd },
    { NULL, NULL }
    };

    再查看load_agent函数实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // Implementation of "load" command.
    static jint load_agent(AttachOperation* op, outputStream* out) {
    // get agent name and options
    const char* agent = op->arg(0);
    const char* absParam = op->arg(1);
    const char* options = op->arg(2);

    // If loading a java agent then need to ensure that the java.instrument module is loaded
    if (strcmp(agent, "instrument") == 0) {
    JavaThread* THREAD = JavaThread::current(); // For exception macros.
    ResourceMark rm(THREAD);
    HandleMark hm(THREAD);
    JavaValue result(T_OBJECT);
    Handle h_module_name = java_lang_String::create_from_str("java.instrument", THREAD);
    JavaCalls::call_static(&result,
    vmClasses::module_Modules_klass(),
    vmSymbols::loadModule_name(),
    vmSymbols::loadModule_signature(),
    h_module_name,
    THREAD);
    if (HAS_PENDING_EXCEPTION) {
    java_lang_Throwable::print(PENDING_EXCEPTION, out);
    CLEAR_PENDING_EXCEPTION;
    return JNI_ERR;
    }
    }

    return JvmtiExport::load_agent_library(agent, absParam, options, out);
    }

    主要作用是加载Agent动态链接库,如果是通过Java instrument API实现的Agent,则加载的是libinstrument动态链接库。然后通过动态链接库中的Agent_OnAttach函数来创建JPLISAgent,从而调用agentmain方法。这一部分内容和libinstrument中的Agent_OnLoad函数来创建JPLISAgent,调用premain方法的逻辑相似

相关开源项目

很多开源项目都用到了Java-Agent,下面列举两个项目,有兴趣可以阅读一下

Arthas

Arthas用到非常重要的技术就是Java-Agent,以及相关的字节码增强等技术,从启动方式就能看出来使用的是动态Agent的方式

代码地址:https://github.com/alibaba/arthas

ja-netfilter

一个Java Instrumentation框架,也是通过Java-Agent实现的,支持插件化。使用的是静态Agent方式

相关文章:https://zhile.io/2021/11/29/ja-netfilter-javaagent-lib.html

代码地址:https://gitee.com/ja-netfilter/ja-netfilter

参考

https://blog.51cto.com/alex4dream/3247542

https://tech.meituan.com/2019/11/07/java-dynamic-debugging-technology.html

https://tech.meituan.com/2022/03/17/java-hotswap-sonic.html

https://mp.weixin.qq.com/s?__biz=MzkyMjIzOTQ3NA==&mid=2247484609&idx=1&sn=8bd852871d656f5a5dcb216f810a273f&source=41#wechat_redirect

分享到: