简介
Java Agent 直译过来叫做 Java 代理,但更多称叫做 Java 探针
Java Agent是一种特殊的Java程序(Jar文件),与普通Java程序通过main方法启动不同,agent并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过Instrumentation API
与虚拟机交互
相关概念
Instrumentation
Instrumentation
是Java提供的一个来自JVM的接口,该接口提供了一系列查看和操作Java类定义的方法,例如修改类的字节码、向classLoader
的classpath
下加入jar文件等。使得开发者可以通过Java语言来操作和监控JVM内部的一些状态,进而实现Java程序的监控分析,甚至实现一些特殊功能(如AOP、热部署)
主流的JVM都提供了Instrumentation
的实现,但是鉴于Instrumentation
的特殊功能,并不适合直接提供在JDK的runtime里,而更适合出现在Java程序的外层,以上帝视角在合适的时机出现。因此如果想使用Instrumentation
功能,「拿到Instrumentation实例,我们必须通过Java agent」,Instrumentation
常用方法如下:
1 | public interface Instrumentation { |
其中最常用的方法就是addTransformer(ClassFileTransformer transformer)
了,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer
接口,定义如下:
1 | /** |
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
代码入口是premain
或agentmain
方法,具体选用哪个方法以及其中的内容根据应用场景决定
配置文件参数说明:
- 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
静态Agent
这种方式是使用premain
作为Agent
的入口方法,以JVM启动参数-javaagent:xxx.jar
方式载入,在Java程序的main
方法执行之前执行
编写
premain
方法,应该包含以下两个方法中的一个:1
2public static void premain(String args, Instrumentation inst);
public static void premain(String args);JVM 会优先加载带
Instrumentation
签名的方法1,加载成功忽略方法2,如果没有Instrumentation
签名的方法,则加载方法2定义一个
MANIFEST.MF
文件,其中必须包含Premain-Class选项将包含
premain
的类与MANIFEST.MF
配置文件打包成一个 jar 包在JVM启动参数中添加
-javaagent:[path]
,其中的path
为对应的Agent
的jar包路径。这样则将Agent
挂载成功,Java程序再执行main
方法前执行
动态Agent
与静态方式不同,动态Agent
允许代理的目标程序的JVM先启动,再通过attach
机制载入
同样需要实现
agentmain
方法,加载优先级与premain
相同,带Instrumentation
签名的方法优先1
2public static void agentmain(String args, Instrumentation inst);
public static void agentmain(String args);定义一个
MANIFEST.MF
文件,与静态Agent
不同的是,此时必须包含Agent-Class选项同样将包含
agentmain
的类与MANIFEST.MF
配置文件打包成一个 jar 包和
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
最常用Instrumentation
的addTransformer
方法对类加载做拦截,对输入的类的字节码进行修改、增强。依赖字节码修改,字节码修改技术主要有 Javassist、ASM,Javassist使用更简单,这里使用Javassist来进行字节码修改,引入相关maven包
1 | <dependency> |
Javassist
使用可以参考下面文档:
基于 Javassist 和 Javaagent 实现动态切面
实现极简的watch命令
模拟Arthas的watch命令,来统计方法执行耗时
开发Agent
https://github.com/ShadowTwj/sandbox/tree/master/agent/src/main/java/cn/tianwenjie/watch
代码
- Agent入口方法,包括
premain
和agentmain
两个方法,后面会分别测试两个场景

实现类转换器,来对指定类和方法增强
配置文件
配置MANIFEST.MF
文件,指定Premain-Class
和Agent-Class
等属性,将配置文件与代码一同打包生成jar包
也可以使用maven的maven-assembly-plugin
插件,来进行打包,参数可直接配置在pom文件中,打包的时候就会自动将配置信息生成 MANIFEST.MF
配置文件打进包里
1 | <plugin> |
测试
分别测试Agent的两种方式
https://github.com/ShadowTwj/sandbox/tree/master/agent-run/src/main/java/cn/tianwenjie/watch
静态Agent测试
- 打印控制台输入,统计打印方法耗时

- 添加
VM options
,指定Agent
Jar包路径

执行
main
方法,观察print
方法耗时
动态Agent测试
测试方法如上,打印控制台输入,统计打印方法耗时
不需要添加
VM options
,直接执行main
方法,观察未挂载Agent
前控制台只打印了输入参数,没有打印方法耗时,该方法不要结束,等待下面Attach
Attach
VM,挂载Agent
Jar包Attach
成功后,再执行测试类,观察到方法增强成功,打印控制台输入的同时打印方法耗时
模拟热加载
模拟热加载,重新加载修改的类
与watch命令最大的区别是没有使用字节码修改技术,而是自定义编译器,将新的代码编译为字节码
开发Agent
https://github.com/ShadowTwj/sandbox/tree/master/agent/src/main/java/cn/tianwenjie/hotdeploy
代码
Agent入口方法,静态Agent对热加载无用,这里只实现动态Agent的
agentmain
方法使用
retransform
和redefineClasses
方法效果一样,这里使用redefineClasses
方法,不在实现类转换器自定义编译器,因为热加载改动代码大多都不可预测,使用字节码修改技术并不方便,这里自定义编译器,来编译成字节码
具体代码:
配置文件
修改maven-assembly-plugin
插件配置,打包生成配置文件
1 | <plugin> |
测试
https://github.com/ShadowTwj/sandbox/tree/master/agent-run/src/main/java/cn/tianwenjie/hotdeploy
测试方法同上watch命令,打印控制台输入
Attach
VM,挂载Agent
Jar包,热加载新类。如下图,继续打印控制台输入,可以看到热加载成功
Java-Agent原理
静态Agent
启动时加载过程
JPLISAgent:作用是初始化所有通过Java Instrumentation API编写的Agent,并且也承担着通过JVMTI实现Java Instrumentation中暴露API的责任
- 创建并初始化 JPLISAgent;
- 监听 VMInit 事件,在 JVM 初始化完成之后做下面的事情:
- 创建 InstrumentationImpl 对象 ;
- 监听 ClassFileLoadHook 事件 ;
- 调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会去调用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 类的 premain 方法 ;
- 解析 javaagent 中 MANIFEST.MF 文件的参数,并根据这些参数来设置 JPLISAgent 里的一些内容。

源码
参数解析
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 (valid_jdwp_agent(name, is_absolute_path)) {
jio_fprintf(defaultStream::error_stream(),
"Debugging agents are not supported in this VM\n");
return JNI_ERR;
}
// 存储Agent解析结果
// name:"instrument",动态链接库
add_init_agent(name, options, is_absolute_path);
}
// -javaagent
} else if (match_option(option, "-javaagent:", &tail)) {
jio_fprintf(defaultStream::error_stream(),
"Instrumentation agents are not supported in this VM\n");
return JNI_ERR;
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;
}
}这段逻辑用来解析需要加载的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));
}加载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
26for (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());
}
}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
59JNIEXPORT 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
48void 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,过程大致如下:
- 创建并初始化JPLISAgent;
- 解析 javaagent 里 MANIFEST.MF 里的参数;
- 创建 InstrumentationImpl 对象;
- 监听 ClassFileLoadHook 事件;
- 调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会去调用javaagent里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法。

Attach
动态Agent是通过Attach机制来加载,下面分析下Attach原理
AttachListener
Attach
机制通过Attach Listener
线程来进行相关事务的处理,AttachListener
初始化如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void 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
21static 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
22LinuxAttachOperation* 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;
}
}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
25public 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;
}
}
}可以看出最终调用
AttachProvider
的attachVirtualMachine
方法,AttachProvider
是抽象类,不同系统不同实现,在MacOS中实现类是BsdAttachProvider
,其中attachVirtualMachine
实现是:1
2
3
4
5public 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
55BsdVirtualMachine(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);
}
}loadAgent方法
通过
attach
方法,连接上目标JVM后,通过loadAgent
方法来加载Agent
,其本质是向目标JVM发送load
命令,这里不再展开1
2
3
4
5
6
7
8
9
10
11
12
13private 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 Listener
的attach_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