说明: 本文很长,长到超出了掘金编辑器的限制字符数 10万,所以我在最后边只是图解,没有更多的文字和代码描述了,本文知识点较多,如果没接触过agent那必然大概率会懵(大部分知识点讲解完后,我都会配个图来总结归纳加强理解)。当你一点点去理解尝试后相信会有所收获,另外水平有限不对地方请指导
本文大概结构: - 前置节:简单认识 ->JVMTI,Java Agent,JVMTIAgent,libinstrument.so (先混个脸熟) - 第一节:Java Agent介绍与(静/动)态加载方式描述+图解。 - 第二节:JVMTI介绍,功能&使用场景以及c语言自实现一个JVMTIAgent - 第三节:Java Agent 静态加载demo+源码分析+图解 - 第四节:Java Agent 动态加载demo+源码分析+图解 - 第五节:自言自语 - 另外由于本文字数限制,所以关于Java Agent的 实际应用 另写了一篇:使用Java Agent 插桩技术:实现 DB中敏感字段自动加解密
本文涉及到的知识点:
- JVMTI(Java Virtual Machine Tool Interface)
- JVMTIAgent
- Java Agent
- Java 类加载机制
- unix 套接字
- 信号机制(signal)
- hotspot源码
- 动态链接库文件 (linux中是 .so ,win中是 .dll 结尾)
- JNI(java native interface)
- 字节码修改(本文使用的是 javaassist之前我的一篇文章有详细介绍:Javassist使用教程【译】)
- 钩子(hook)机制:在编程中这个非常重要,不管是底层(如linux)还是上层框架(如spring),此机制都会给软件带来很大的扩展空间和灵活性,是编程中非常常见的一种技术,在下文中回调函数其实就是指的钩子函数,钩子是机制,回调是动作,本文中你叫他钩子函数或者回调函数都是一个意思。
0、前置说明
在开始之前,我们先来了解几个重要的内容,先对这些东西有个大体概念。
JVMTI:(全称: Java Virtual Machine Tool Interface)是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展,JVMTI源码在jdk8/jdk8u/jdk/src/share/javavm/export/jvmti.h这个文件中,截图如下:

image.png
Java Agent:可以使用Java语言编写的一种agent,编写他(后边会讲到)的话会直接使用到jdk中的 Instrumentation API(在sun.instrument和java.lang.instrument和com.sun.tools.attach包中)。libinstrument.so:说到Java Agent必须要讲的是一个叫做instrument 的 JVMTIAgent(linux下对应的动态库是libinstrument.so),因为本质上是直接依赖它来实现Java Agent的功能的,另外instrument agent还有个别名叫 JPLISAgent (Java Programming Language Instrumentation Services Agent),从这名字里也完全体现了其最本质的功能:就是专门为java语言编写的插桩服务提供支持的。(在这里多出来个词叫 插桩,知道的就罢了,不知道的话姑且可以简单对等理解为:AOP中的增强)。下边是我安装的openJdk11上的libinstrument.so文件,可以看到他存在于我的JAVA_HOME/lib/目录下,其中就包含了Agent_OnLoad,Agent_OnAttach,Agent_OnUnload三个我们比较关注的函数,截图如下:

image.png
当我们静态加载agent jar(启动时添加vm参数 -javaagent:xxxjar包路径的方式)时Agent_OnLoad会调用到我们的premain方法,当我们动态加载(JVM的attach机制,通过发送load命令来加载)时Agent_OnAttach会调用到我们的agentmian方法。也许你现在不明白这个,但是当你看了下边的第三&四节源码后你就能串起来了。
Instrumentation API:为Java Agent提供了一套Java层面的接口,它是在Java 5开始引入的,旨在为Java开发者提供一种标准方式来动态修改类的行为以及做增强操作,部分示例:

image.png
JVMTIAgent:是一个动态链接库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:- Agent_OnLoad函数: 会在静态加载agent jar时调用。
- Agent_OnAttach函数: 如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用该函数。
- Agent_OnUnload函数: 在agent卸载时调用。
- 更强大的功能: 在我们使用Java Agent时不管是静态加载还是动态加载,其实实现的功能比较有限,基本上也就是下边这些:
静态加载可以实现:类加载时修改(transform)/(redefine)重定义类字节码动态加载可以实现:运行时修改类字节码,dump线程堆栈信息,获取系统配置等。动态加载实现的功能 完整的无非就是下边这几个:

image.png
-
- 而如果你直接去使用c编写一个JVMTIAgent, 那能实现的功能就比较多了,你可以根据需要实现JVMTI预留出的每一个钩子函数,从而在指定的时机来让jvm加载你的逻辑以达到你的目的,这就是钩子函数的灵活之处。
以上几个知识点之间的关系图如下: ps:(牢记这几个知识点之间的关系与各自的功能,会使我们理解本文起到事半功倍的效果!!!)

image.png
接下来,我们深入展开讲解下以上这些知识点。
1、Java Agent
Java Agent 是什么?
Java Agent是Java平台提供的一种特殊机制,它允许开发者 在Java应用程序 (被jvm加载 / 正在被jvm运行) 时 注入我们指定的字节码。这种技术被广泛应用于 功能增强、监控、性能分析、调试、信息收集等多种场景 , Java Agent 依赖于 instrument 这个特殊的 JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的, Java Agent有两种加载时机,分别是:
Java Agent 加载方式
静态加载
- 静态加载:即 JVM启动时加载,在JVM启动时通过命令行参数
-javaagent:path/to/youragent.jar指定Agent的 jar包。这要求Agent的入口类(即agent.jar包中的META-INF->MAINIFEST.MF文件中的Premain-Class对应的类)实现premain方法,该方法会在应用程序的main方法之前执行。这一机制使得我们可以修改应用程序的类或执行其他初始化任务,这种机制对于性能监控、代码分析、审计或增强等场景非常有用。
实现步骤: (文字描述)
注意: (这里只简单文字描述,详细内容和源码放到后边讲解)
- 编写Agent代码: 开发一个Java类,实现
premain方法并在其中将类转换的实现类添加到Instrumentation实例。这个方法是静态加载Agent的入口点,premian将在vm初始化时被调用。 编写转换增强(使用字节码工具比如javaassist 或ASM )逻辑 需要实现ClassFileTransformer类的transform方法,此方法在vm初始化(VMInit)阶段被注册,在类加载时被调用 - 打包Agent: 将Agent类和可能依赖的其他类打包成一个JAR文件。在Agent JAR的
MANIFEST.MF文件中,必须要有Premain-Class属性,该属性的值是包含premain方法的类的全限定名。(一般我们通过maven打包插件来打包Agent Jar包,同样的,MANIFEST.MF文件中的内容也是通过插件来生成的) - 启动被插桩程序时指定Agent: 在启动被插桩程序时,通过添加
-javaagent:/path/to/youragent.jar参数来指定Agent JAR。如果需要传递参数给Agent,可以在JAR路径后添加=符号和参数字符串,如-javaagent:/path/to/youragent.jar=config1=value1,config2=value2
动态加载
- 动态加载:即 在JVM运行应用程序时任意时刻加载,在JVM运行时加载Agent,这通常通过使用JDK的Attach API实现(本质上是使用unix套接字实现了同一机器不同进程间的通信)。这要求Agent实现
agentmain方法,该方法可以在java应用程序运行过程中任意时刻被调用。具体实现方式文字描述(后边我们会演示通过代码方式如何实现):
实现步骤:(文字描述)
注意: (这里只简单文字描述,详细内容和源码放到后边讲解)
动态加载Java Agent主要依赖于Java Instrumentation API的agentmain方法和Attach API。具体步骤如下: 1. 准备Agent JAR: 与静态加载相同,需要准备一个包含agentmain方法的Agent JAR文件。agentmain方法是动态加载Agent时由JVM调用的入口点。该JAR文件还需要在其MANIFEST.MF中声明Agent-Class属性,指向包含agentmain方法的类。 编写转换增强(使用字节码工具比如javaassist 或ASM )逻辑 需要实现ClassFileTransformer类的transform方法,与静态加载不同,此方法的调用需要通过inst.retransformClasses(“要重新加载的类”);来触发。 1. 使用Attach API: Attach API允许一个运行中的Java进程连接(通过UNIX套接字)到另一个Java进程。一旦连接,它可以用来加载Agent JAR。这通常通过使用com.sun.tools.attach.VirtualMachine类实现,该类提供了附加到目标JVM进程并加载Agent的方法 1. 加载Agent: 通过Attach API附加到目标JVM后,可以指定Agent JAR路径并调用loadAgent或loadAgentLibrary方法来加载并初始化Agent。加载后,JVM会调用Agent JAR中定义的agentmain方法。 如果你只是对java代码进行插桩或者一些dump操作等(则只使用libinstrument.so就够了)这时就可以调用loadAgent(这个方法内部就是写死的去加载 libinstrument.so这个动态链接库) 。 而如果想加载(你自己用c实现的JVMTIAgent)编译后的自己的动态链接库,则需使用loadAgentLibrary传入你想要加载的动态链接库名称,比如 传入的是myAgent 则最终会去找(假设是linux)libmyAgent.so这个链接库中的 Agent_OnAttach的方法来执行。
上边我们也提到过JVMTI,而如果你学习了解agent 那么深入理解JVMTI将是必不可少要学习的。下边就来详细说下
2、JVMTI
JVMTI 简介
JVMTI全称:(Java Virtual Machine Tool Interface) ,简单来说就是jvm暴露出来的一些供用户扩展的回调接口集合,有一点我们要知道,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件对应的回调接口。而通过这个回调机制,我们实际上就可以 实现与JVM 的 “互动”。可不要小看这个回调机制,他是n多个框架的底层依赖,没有这个JVMTI回调机制,这些框架也许不能诞生或者需要使用其他更复杂的技术。既然回调机制如此重要,那么都有哪些回调呢?让我们从源码中获取这个内容,如下:
以下是 hotspot 的 JVMTI 中定义的一系列回调函数,(暂时我们定义这段代码片段为 code1,以便后边引用 ):
源码在: /jdk8u/jdk/src/share/javavm/export/jvmti.h
/* Event Callback Structure */
/* 为了方便,我直接把代码和注释搞一行里了。 */
typedef struct {
/* 50 : VM Initialization Event jvm初始化 本文后续会讲解到这个,就是在这一步
设置的类加载时的回调函数和调用的premain方法 */
jvmtiEventVMInit VMInit;
jvmtiEventVMDeath VMDeath;/* 51 : VM Death Event jvm销毁 */
jvmtiEventThreadStart ThreadStart;/* 52 : Thread Start 线程启动 */
jvmtiEventThreadEnd ThreadEnd;/* 53 : Thread End 线程结束 */
jvmtiEventClassFileLoadHook ClassFileLoadHook;/* 54:ClassFileLoadHook类文件加载类加载时会调用*/
jvmtiEventClassLoad ClassLoad; /* 55 : Class Load */
jvmtiEventClassPrepare ClassPrepare;/* 56 : Class Prepare */
jvmtiEventVMStart VMStart; /* 57 : VM Start Event */
jvmtiEventException Exception; /* 58 : Exception */
jvmtiEventExceptionCatch ExceptionCatch;/* 59 : Exception Catch */
jvmtiEventSingleStep SingleStep;/* 60 : Single Step */
jvmtiEventFramePop FramePop;/* 61 : Frame Pop */
jvmtiEventBreakpoint Breakpoint;/* 62 : Breakpoint */
jvmtiEventFieldAccess FieldAccess;/* 63 : Field Access */
jvmtiEventFieldModification FieldModification;/* 64 : Field Modification */
jvmtiEventMethodEntry MethodEntry;/* 65 : Method Entry */
jvmtiEventMethodExit MethodExit;/* 66 : Method Exit */
jvmtiEventNativeMethodBind NativeMethodBind;/* 67 : Native Method Bind */
jvmtiEventCompiledMethodLoad CompiledMethodLoad;/* 68 : Compiled Method Load */
jvmtiEventCompiledMethodUnload CompiledMethodUnload; /* 69 : Compiled Method Unload */
jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;/* 70 : Dynamic Code Generated */
jvmtiEventDataDumpRequest DataDumpRequest;/* 71 : Data Dump Request */
jvmtiEventReserved reserved72;
jvmtiEventMonitorWait MonitorWait;/* 73 : Monitor Wait */
jvmtiEventMonitorWaited MonitorWaited;/* 74 : Monitor Waited */
jvmtiEventMonitorContendedEnter MonitorContendedEnter;/* 75 : Monitor Contended Enter */
jvmtiEventMonitorContendedEntered MonitorContendedEntered; /* 76 : Monitor Contended Entered */
jvmtiEventReserved reserved77;/* 77 */
jvmtiEventReserved reserved78;/* 78 */
jvmtiEventReserved reserved79;/* 79 */
jvmtiEventResourceExhausted ResourceExhausted;/* 80 : Resource Exhausted */
jvmtiEventGarbageCollectionStart GarbageCollectionStart; /* 81 : Garbage Collection Start */
jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;/* 82 : Garbage Collection Finish */
jvmtiEventObjectFree ObjectFree;/* 83 : Object Free */
jvmtiEventVMObjectAlloc VMObjectAlloc;/* 84 : VM Object Allocation */
} jvmtiEventCallbacks;
基于上边code1的代码我们总结归类下大概是这样:(实际上本文的agent只是和ClassFileLoadHook以及 VMInit这俩有关,其他的我们了解即可,当然除了这俩之外我们也是可以在其他节点(下边规定的这些节点)扩展实现JVMTI的一系列回调函数,不过需要使用c实现)
VM 生命周期事件:
VMInit: 当虚拟机初始化时触发,在此时会注册类加载时的回调函数和调用的premain方法(在源码小节会说到)。
VMDeath: 当虚拟机终止之前触发。 VMStart: 在虚拟机启动期间,任何Java代码执行之前触发。 类加载事件:
ClassFileLoadHook:类加载时调用此钩子函数的实现ClassFileTransformer 的transform
ClassLoad: 类加载到虚拟机后触发。 ClassPrepare: 类所有静态初始化完成,所有静态字段准备好,且所有方法都已绑定后触发。 线程事件:
ThreadStart: 线程启动时触发。
ThreadEnd: 线程结束时触发。 方法执行事件:
MethodEntry: 进入方法时触发。
MethodExit: 退出方法时触发。 异常事件:
Exception: 方法执行过程中抛出异常时触发。
ExceptionCatch: 方法捕获到异常时触发。 监控和编译事件
MonitorContendedEnter: 线程尝试进入已被其他线程占用的监视器时触发。
MonitorContendedEntered: 线程进入已被其他线程占用的监视器后触发。 MonitorWait: 线程等待监视器的notify/notifyAll时触发。 MonitorWaited: 线程等待监视器的notify/notifyAll结束后触发。 CompiledMethodLoad: 方法被编译时触发。 CompiledMethodUnload: 编译的方法被卸载时触发。 字段访问和修改事件:
FieldAccess: 访问字段时触发。
FieldModification: 修改字段时触发。 其他事件:
GarbageCollectionStart: 垃圾收集开始时触发。
GarbageCollectionFinish: 垃圾收集完成时触发。 DataDumpRequest: 请求转储数据时触发。
这些事件回调为Java应用和工具提供了深入虚拟机内部操作的能力,从而能够进行更加精细的监控和调试。开发者可以根据需要注册监听特定的事件,本质上也就是我们说的开发者与JVM的 ”互动“。
接下来我们看下JVMTI的主要功能,其实如果你看了上边的回调节点,基本上可以猜到他主要能干些啥,因为这些功能都是靠实现上边这些回调节点来开发的。
JVMTI 的主要功能&使用场景
功能: 1. 事件通知:JVMTI允许工具通过事件获取JVM内发生的特定情况的通知,如线程启动/结束、类加载/卸载、方法进入/退出等。 1. 线程管理:它提供了监控和管理Java程序中线程状态的能力。 1. 堆和垃圾回收:JVMTI支持查询堆信息、监控垃圾回收事件,以及在某些条件下控制垃圾回收的执行。 1. 调试支持:JVMTI为调试器提供了丰富的接口,支持断点、单步执行、字段访问/修改等调试功能。 1. 性能监测:提供了监视和分析JVM性能的工具,如获取方法执行时间、内存使用情况等。
场景: 1. 开发调试工具:利用JVMTI提供的调试支持,开发强大的调试工具,比如 idea ,eclipse等等。 1. 性能分析:构建性能分析工具来识别Java应用的性能瓶颈。 1. 监控工具:创建监控工具来实时监视JVM的健康状况和性能指标。 1. 覆盖率分析:通过跟踪类和方法的加载与执行,帮助生成代码覆盖率报告
文字描述你可能感觉不到什么,但是如果提到这些框架,你大概率会知晓其中的一个或者几个,而他们就是基于Java Agent 实现,而Java Agent本质上是需要依赖JVMTI的,所以可以说这些大名鼎鼎的框架 直接/间接 上都是 依赖了JVMTI,比如下边这些:
运行时监控&性能分析类: - VisualVM:是JDK自带的一个用于Java程序性能分析的可视化工具,通过他可以获取应用程序的,堆,内存,线程,cpu,快照等等运行时信息。 - JProfiler:和VisualVM类似,也是能获取Java应用程序以及jvm的各种信息。 - BTrace:是一个监控&追踪工具,可以监控程序状态,获取运行时数据信息,如方法返回值,参数,调用次数,全局变量,调用堆栈等。 - Arthas: 是阿里的一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率 - Greys:是一个JVM进程执行过程中的异常诊断工具,可以在不中断程序执行的情况下轻松完成问题排查工作。其实他也是模仿了BTrace
热加载类: - HotSwapAgent:是一个免费的开源插件,它扩展了JVM内置的HotSwap机制的功能 - reload: - JRebel:是一个商业化的Java热加载工具,它使开发者能够在不重启JVM的情况下,实时地重新加载改动后的类文件 - spring-loaded:是一个开源的热加载工具,主要用于Spring框架,但也可以用于非Spring应用。 - Spring Boot DevTools: 是 Spring Boot 的一个模块,提供了诸多功能其中包括热加载。
链路追踪类 - skywalking:是一个开源的应用性能监控(APM)工具,主要用于监控、追踪、诊断分布式系统,特别是基于微服务、云原生和容器化(Docker, Kubernetes, Mesos)架构的大规模分布式系统。SkyWalking 提供全面的解决方案,包括服务性能监控、服务拓扑分析、服务和服务实例性能分析,以及对调用链路的追踪和诊断,可以看到他的功能很强大也很多,其中链路追踪只是他的一部分功能。 - Pinpoint :也是一个链路追踪APM框架,支持java和php。
开发调试类: - IDEA 的 debug(这也是我们天天用的功能):比如我们在启动项目时,idea会自动加上这个jar,如下:

image.png
这个jar其实就负责IDEA与JVM之间的 通信,执行例如设置断点、暂停和恢复执行、修改字段值等调试指令,同时他还可以收集Java 应用运行状态的数据,例如方法调用、变量状态、线程信息等。这样我们在debug时就可以看到那么多的数据啦。注意: idea debug 其实不单单仅靠一个agent实现,他的实现是基于Java Platform Debugger Architecture (JPDA),即Java 平台调试架构,这个架构包含3部分 (JVMTI(JVM Tool Interface)、JDWP(Java Debug Wire Protocol)、JDI(Java Debug Interface))所以说我们启动项目时看到的 debuger-agent.jar 只是使用了JVMTI这部分。具体debug功能如何实现我们不过多展开了。 - eclipse 的 debug这位功臣现在似乎用的不多了,但是我猜测它的debug肯定也是要依赖JVMTI的。
包括在我的链路追踪文章中使用 的ttl agent方式也是依赖了JVMTI。
当然,肯定还有很多我不知道的框架亦或者插件直接或者间接使用到了JVMTI,这里我们不过多讨论了。 上边简单介绍了JVMTI是什么,以及他的功能和使用场景,以及一些直接/间接使用到他的框架。下边我们就看看如何直接实现JVMTI Agent。
使用c编写一个JVMTIAgent,需要实现JVMTI的 ClassFileLoadHook 这个钩子函数
在JVMTI简介中我们看到很多JVMTI的回调节点,而这些函数的定义都在 hotspot/jdk/src/share/javavm/jvmti.h 这个文件中,如下:

image.png
可以看到有很多回调钩子(本文所讲的Java Agent其实只是用到了 类加载时的回调 这么一个函数),只要实现了这些钩子,jvm会在执行到这些钩子对应的时机,去勾取对应的实现。从而完成 开发者 与 jvm 的 “互动”。 另外 JVMTI工作在更接近JVM核心的层面,提供了比Java Agent通过Instrumentation API更底层、更广泛的控制能力。例如,JVMTI可以用来实现复杂的调试器或性能分析工具,这些工具需要在JVM内部进行深入的操作,而这些操作可能超出了纯Java代码(即使是通过Instrumentation API)能够实现的范围,更多的情况是需要使用c/c++语言来实现。
比如说我们最常见的也是在本文要讲的,即,想在某个类的字节码文件读取之后类定义之前能修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那我们就可以实现一个回调函数赋给JvmtiEnv (JvmtiEnv是一个指针 指向JVMTI的数据结构,在JVMTI中每个agent都通过这个JvmtiEnv与JVM交互)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数里来了。 而有一点我们要知道,就是在Java的Instrumentation API引入之前(Java 5之前),想实现ClassFileLoadHook这个钩子函数(即在类字节码加载到JVM时进行拦截和修改)我们只能是编写原生代码也就是c/c++代码来实现(当然你可以使用代理或者覆盖类加载器的loadClass方法,这里我们不做讨论),而在Java 5之后引入了Instrumentation API ,所以我们能像现在这样,通过以下这种java代码实现,

image.png
如果是Java 5之前?对不起,你只能是通过原生来实现也就是c/c++代码。
我们下边就给他使用c代码实现一个 JVMTI中 ClassFileLoadHook, 这个钩子函数中的逻辑比较简单,它演示了如何使用c语言设置ClassFileLoadHook事件回调,并在回调函数中简单地打印被加载的类的名称(注意: 此处小案例使用了启动时静态加载,如果要动态加载需要实现 Agent_OnAttach函数,这里我们不做演示)。步骤如下:
1. 创建JVMTI Agent:
创建一个名为ClassFileLoadHookAgent.c的C文件,用于实现JVMTI Agent: ```c
include
include
include
// ClassFileLoadHook回调函数 void JNICALL ClassFileLoadHook( jvmtiEnv jvmti_env, JNIEnv jni_env, jclass class_being_redefined, jobject loader, const char name, jobject protection_domain, jint class_data_len, const unsigned char class_data, jint* new_class_data_len, unsigned char new_class_data) { // 打印即将加载的类的名称 if (name != NULL) { printf("使用c编写ClassFileLoadHook的实现_当前加载的类名称是: %s\n", name); } } // Agent_OnLoad,JVMTI Agent的入口点 JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM jvm, char options, void reserved) >{ jvmtiEnv jvmti = NULL; jvmtiCapabilities capabilities; jvmtiEventCallbacks callbacks; jvmtiError err; // 获取JVMTI环境 jint res = (*jvm)->GetEnv(jvm, (void )&jvmti, JVMTI_VERSION_1_2); if (res != JNI_OK || jvmti == NULL) { printf("ERROR: Unable to access JVMTI Version 1.2 (%d)\n", res); return JNI_ERR; }
// 设置所需的能力 (void)memset(&capabilities, 0, sizeof(jvmtiCapabilities)); capabilities.can_generate_all_class_hook_events = 1; err = ( jvmti)->AddCapabilities(jvmti, &capabilities); if (err != JVMTI_ERROR_NONE) { printf("ERROR: Unable to AddCapabilities (%d)\n", err); return JNI_ERR; } // 设置 ClassFileLoadHook 回调事件 (void)memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &ClassFileLoadHook; err = (jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks)); if (err != JVMTI_ERROR_NONE) { printf("ERROR: Unable to SetEventCallbacks (%d)\n", err); return JNI_ERR; } // 启用 ClassFileLoadHook 事件 err = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, >JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); if (err != JVMTI_ERROR_NONE) { printf("ERROR: Unable to SetEventNotificationMode for ClassFileLoadHook >(%d)\n", err); return JNI_ERR; } return JNI_OK; } ```
2. 编译Agent: 编译这个Agent需要依赖于你的操作系统和JDK安装路径。例如,在我的Linux (centos7) 上,则使用以下gcc命令来进行编译:bash gcc -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -o >classfileloadhookagent.so ClassFileLoadHookAgent.c这里${JAVA_HOME}是你JDK的安装目录,这条命令会生成一个名为classfileloadhookagent.so的共享库(动态链接库 linux中一般以 .so 结尾之前说过了)文件。
3. 运行Agent: 使用-agentpath参数将你的Agent附加到Java应用程序。并使用java命令执行编译后的class文件,如下:bash java -agentpath:/usr/local/src/agent/classfileloadhookagent.so NativeCodeImplClassFileLoadHookTest当Java应用程序运行时,每当类文件被加载前,你的ClassFileLoadHook回调函数将被触发,打印出即将加载的类的名称,接下来我们实操&演示下。
实操&演示
下面进行演示,如下:
(注意代码中是去掉包名的因为这样我们只需要java NativeCodeImplClassFileLoadHookTest就可以执行class文件了,有包名的话还得全限定所以我们就不加包名了)

image.png

image.png

image.png
可以看到通过在 ClassFileLoadHookAgent.c中实现函数 Agent_OnLoad并设置&开启回调事件ClassFileLoadHook,成功的让jvm在加载类时调用了回调函数,也就是执行了这段代码: printf("使用c编写ClassFileLoadHook的实现_当前加载的类名称是: %s\n", name); 看到这里 你会不通过java instrument api的方式编写JVMTI的回调了吗? 其他的回调函数其实也类似,这里我们只演示了 ClassFileLoadHook这个回调如何实现 。
上边我们讲解了Java Agent和JVMTI以及如何实现一个JVMTIAgent,到这里相信你已经有所了解,接下来我们就编写几个agent案例并分别分析他们的实现原理以及源码流程。让我们对 agent 的工作机制以及底层实现 有更深入的认识。
ps: 静态加载和动态加载区别还是比较大的,所以我打算把他们分开各说各的,以免混淆。
3、Java Agent 静态加载演示、图解、源码分析
静态加载demo实现与演示
(一些比较细的东西,我都放到代码注释中了,在代码外就不额外啰嗦了)
想要达到的效果
通过agent插桩的方式修改Date类的getTime()方法,使其返回的时间戳为:秒级别而不是毫秒级,如下是Date类的getTime方法一览:

image.png
通过Instrument API和javaassist 编写插桩代码:
关于javaassist如果不了解的话,可以参考我的上一篇文章:Javassist使用教程【译】
package com.xzll.agent.config;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @Author: 黄壮壮
* @Date: 2023/3/3 09:15:21
* @Description:
*/
public class JdkDateAgentTest {
public static void premain(String args, Instrumentation inst) throws Exception {
//调用addTransformer()方法对启动时所有的类(应用层)进行拦截
inst.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//操作Date类
if ("java/util/Date".equals(className)) {
CtClass clazz = null;
System.out.println("对date执行插桩 【开始】");
try {
// 从ClassPool获得CtClass对象 (ClassPool对象是CtClass对象的容器,CtClass对象是类文件的抽象表示)
final ClassPool classPool = ClassPool.getDefault();
clazz = classPool.get("java.util.Date");
//获取到java.util.Date类的 getTime方法
CtMethod getTime = clazz.getDeclaredMethod("getTime");
//(修改字节码) 这里对 java.util.Date.getTime() 方法进行了改写,先打印毫秒级时间戳,然后在return之前给他除以1000(变成秒级) 并返回。
String methodBody = "{" +
"long currentTime = getTimeImpl();" +
"System.out.println(" 使用agent探针对Date方法进行修改并打印,当前时间【毫秒级】:"+currentTime );" +
"return currentTime/1000;" +
"}";
getTime.setBody(methodBody);
//通过CtClass的toBytecode(); 方法来获取 被修改后的字节码
return clazz.toBytecode();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (null != clazz) {
//调用CtClass对象的detach()方法后,对应class的其他方法将不能被调用。但是,你能够通过ClassPool的get()方法,
//重新创建一个代表对应类的CtClass对象。如果调用ClassPool的get()方法, ClassPool将重新读取一个类文件,并且重新创建一个CtClass对象,并通过get()方法返回
//如下所说:
//detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
}
System.out.println("对date执行插桩【结束】");
}
}
return classfileBuffer;
}
}
}
配置打包时的方式和MAINFSET.MF数据在pom中
配置maven打包方式与数据 (我这里使用assembly打包),pom代码如下:
org.apache.maven.plugins
maven-compiler-plugin
11 11 org.apache.maven.plugins maven-assembly-plugin 2.4.1 jar-with-dependencies 黄壮壮 ${maven.build.timestamp} com.xzll.agent.config.JdkDateAgentTest true false false make-assembly package single
使用mvn package 命令打包

image.png
解压jar 并查看/META-INF/MANIFEST.MF文件内容
使用命令解压jar:
unzip ~/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar -d ~/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencie
查看/META-INF/MANIFEST.MF文件内容:

image.png
编写&执行main方法(使用-javaagent静态加载上边的agent jar包)
编写并执行main方法,这里我们很重要的一步就是在 vm参数中配置了 此内容:
-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
也就是我们所说的: 静态加载。

image.png
看下效果:

image.png
可以看到,在main方法启动时添vm参数(即:
-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
)从而让jvm启动时(也即静态)加载我们编写的agent jar ,使得在执行main方法里的getTime方法时执行的是我们修改替换(transform)后的,修改后的 getTime 方法体内容是:
{
long currentTime = getTimeImpl();
System.out.println(" 使用agent探针对Date方法进行修改并打印,当前时间【毫秒级】:"+currentTime );
return currentTime/1000;
}
,因此让Date getTime()方法返回了秒级时间戳。,这就是所谓的 插桩。是不是有点aop的意思?
以上就是静态加载的demo了,虽然很简单,但是麻雀虽小五脏俱全了也算是,趁热打铁吧,下边我们就从 源码角度来逐步分析静态加载实现的流程与原理 ,注意 源码小节比较重要 ,看完源码,才会有恍然大悟的感觉。没错我就是这个感觉。
静态加载源码解析
解析启动时传入的vm参数
源码这一节我准备从源头说起,我们知道静态加载agent时我们必须使用-javaagent:xxx.jar 而我们就从这里说起,看看jvm到底是如何解析运作的,首先第一步传入的参数jvm得认识吧?所以就来到了 解析参数这一步,解析参数的入口在这里:

image.png
接下来到 parse_each_vm_init_arg 这个里边,而这个函数的内容超级多,因为我们知道vm参数巨多,所以这个里边的代码也巨长,但是我们这里只关心-javaagent,其他的我们知道了解即可,
完整代码在: /hotspot/src/share/vm/runtime/arguments.cpp 中
jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args,
SysClassPath* scp_p,
bool* scp_assembly_required_p,
Flag::Flags origin) {
.......略掉n多行代码.......
接下的参数有很多,随便举几个比较熟悉/听过的吧:
-Xbootclasspath
-Xmn
-Xms
-Xmx
-XX:MaxHeapSize=
-XX:ReservedCodeCacheSize
-XX:IncreaseFirstTierCompileThresholdAt
-XX:+CMSPermGenSweepingEnabled
-XX:+UseGCTimeLimit
-XX:TLESize
-XX:TLEThreadRatio
-XX:CMSParPromoteBlocksToClaim
-XX:CMSMarkStackSize
-XX:ParallelCMSThreads
-XX:MaxDirectMemorySize
//与agent相关的,可以看到 不管是 -agentlib 还是-agentpath还是-javaagent,
//最终都会执行到一个函数即:add_init_agent
#endif // !INCLUDE_JVMTI
add_init_library(name, options);
}
// -agentlib and -agentpath
} else if (match_option(option, "-agentlib:", &tail) ||
(is_absolute_path = match_option(option, "-agentpath:", &tail))) {
if(tail != NULL) {
const char* pos = strchr(tail, '=');
size_t len = (pos == NULL) ? strlen(tail) : pos - tail;
char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1, mtInternal), tail, len);
name[len] = '\0';
char *options = NULL;
if(pos != NULL) {
size_t length = strlen(pos + 1) + 1;
options = NEW_C_HEAP_ARRAY(char, length, mtInternal);
jio_snprintf(options, length, "%s", pos + 1);
}
#if !INCLUDE_JVMTI
#endif // !INCLUDE_JVMTI
add_init_agent(name, options, is_absolute_path);
}
// -javaagent
} else if (match_option(option, "-javaagent:", &tail)) {
#else
if(tail != NULL) {
size_t length = strlen(tail) + 1;
char *options = NEW_C_HEAP_ARRAY(char, length, mtInternal);
jio_snprintf(options, length, "%s", tail);
//此处传入的 instrument 会被在前边加上 lib ,
//在后边加上.so 也就是最终的 libinstrument.so 看到这个相信已经很熟悉了
//这就是我们使用-javaagent时 底层所使用的 动态库文件名,该函数在上边有介绍,忘记的回去看看。
add_init_agent("instrument", options, false);
}
.......略掉n多行代码.......
//而这个里边就是很简单的一件事,即构建Agent Library链表,也就是说将我们vm中传入的jar路径以及后边的参数存放起来然后待后续使用。
static AgentLibraryList _agentList;
static void add_init_agent(const char* name, char* options, bool absolute_path)
{ _agentList.add(new AgentLibrary(name, options, absolute_path, NULL)); }
可以看到无论是 -agentlib还是-agentpath还是-javaagent 都会执行 add_init_agent 函数,而这个函数就是一个目的:构建Agent Library链表。也就是说将我们vm中传入的jar路径以及后边的参数存放起来(放到了 _agentList 链表中),然后 待后续使用。
创建JVM并调用create_vm_init_agents函数
解析完参数后,就来到了创建并启动jvm的环节,创建并启动jvm做的工作很多,我只保留了和agent相关的代码,如下:
此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
...略去n行代码
// Convert -Xrun to -agentlib: if there is no JVM_OnLoad
// Must be before create_vm_init_agents()
if (Arguments::init_libraries_at_startup()) {
convert_vm_init_libraries_to_agents();
}
// Launch -agentlib/-agentpath and converted -Xrun agents
if (Arguments::init_agents_at_startup()) {
create_vm_init_agents();
}
...略去n行代码
}
从注释上可以看出有一个转换 -Xrun为 -agentlib 的操作,而-Xrun 是 Java 1.4 及之前版本用于加载本地库(native libraries)使用的,尤其是用于加载性能分析或调试工具的老旧方式。从 Java 1.5 开始,推荐使用 -agentlib 作为替代,这是因为 -agentlib 提供了更标准化和更简单的方式来加载和管理 Java Agent,有这个代码的存在是为了更好的向下兼容。这里我们知道这么个事就行了,重点关注下边的逻辑。即:create_vm_init_agents();,这个方法就是创建&初始化agent的入口方法了。此方法内容如下:
遍历agents链表并调用lookup_agent_on_load找到某个动态链接中的Agent_OnLoad函数,并执行
此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中
// Create agents for -agentlib: -agentpath: and converted -Xrun
// Invokes Agent_OnLoad
// Called very early -- before JavaThreads exist
void Threads::create_vm_init_agents() {
extern struct JavaVM_ main_vm;
AgentLibrary* agent;
JvmtiExport::enter_onload_phase();
for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
//lookup_agent_on_load主要功能就是找到动态链接文件,然后找到里面的Agent_Onload方法并返回
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());
}
}
JvmtiExport::enter_primordial_phase();
}
//下边这小段代码在:/hotspot/src/share/vm/runtime/arguments.hpp 中
//说明:上边的 create_vm_init_agents方法中的 Arguments::agents() ,
//其实就是从agent链表中取第一个,代码为:
static AgentLibrary* agents() { return _agentList.first(); }
这个方法的主要作用就是: - 遍历我们刚刚在参数解析时根据-javaagent的值构建的agents链表 - 依次调用lookup_agent_on_load函数来找动态链接文件(在识别到我们vm参数中的-javaagent时,最终找的动态链接文件就是 libinstrument.so 文件) - 在找到后保存到了一个entry结构中,之后来执行这个entry中的方法, 也即:动态链接libinstrument.so中的Agent_OnLoad 方法。
紧接着我们大概看下是怎么找的
通过 lookup_on_load 来查找libinstrument.so文件以及他的Agent_OnLoad方法
此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中
// Find the Agent_OnLoad entry point
static OnLoadEntry_t lookup_agent_on_load(AgentLibrary* agent) {
const char *on_load_symbols[] = AGENT_ONLOAD_SYMBOLS;
//调用lookup_on_load
return lookup_on_load(agent, on_load_symbols, sizeof(on_load_symbols) / sizeof(char*));
}
// Find a command line agent library and return its entry point for
// -agentlib: -agentpath: -Xrun
// num_symbol_entries must be passed-in since only the caller knows the number of symbols in the array.
static OnLoadEntry_t lookup_on_load(AgentLibrary* agent, const char *on_load_symbols[], size_t num_symbol_entries) {
OnLoadEntry_t on_load_entry = NULL;
void *library = NULL;
if (!agent->valid()) {
char buffer[JVM_MAXPATHLEN];
char ebuf[1024];
const char *name = agent->name();
const char *msg = "Could not find agent library ";
// First check to see if agent is statically linked into executable
if (os::find_builtin_agent(agent, on_load_symbols, num_symbol_entries)) {
library = agent->os_lib();
} else if (agent->is_absolute_path()) {
library = os::dll_load(name, ebuf, sizeof ebuf);
if (library == NULL) {
const char *sub_msg = " in absolute path, with error: ";
size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
// If we can't find the agent, exit.
vm_exit_during_initialization(buf, NULL);
FREE_C_HEAP_ARRAY(char, buf, mtThread);
}
} else {
// Try to load the agent from the standard dll directory
if (os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(),
name)) {
library = os::dll_load(buffer, ebuf, sizeof ebuf);
}
if (library == NULL) { // Try the local directory
char ns[1] = {0};
if (os::dll_build_name(buffer, sizeof(buffer), ns, name)) {
library = os::dll_load(buffer, ebuf, sizeof ebuf);
}
if (library == NULL) {
const char *sub_msg = " on the library path, with error: ";
size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
// If we can't find the agent, exit.
vm_exit_during_initialization(buf, NULL);
FREE_C_HEAP_ARRAY(char, buf, mtThread);
}
}
}
agent->set_os_lib(library);
agent->set_valid();
}
//Find the OnLoad function. 查询OnLoad方法 ,其实最终内部会在查询时将Agent加到前边,
//也就是会变成这样: Agent_On(Un)Load/Attach<_lib_name> 了解即可
on_load_entry =
CAST_TO_FN_PTR(OnLoadEntry_t, os::find_agent_function(agent,
false,
on_load_symbols,
num_symbol_entries));
return on_load_entry;
}
注意,因为本小节我们分析的是静态加载,所以只关注-javaagent这个逻辑,解析这个参数时 传入add_init_agent方法的第三个参数 是false,

image.png
而这个参数就是 AgentLibrary的 is_absolute_path,所以根据这里我们可以得出 当使用-javaagent这种方式静态加载Java Agent时 走的是lookup_on_load方法的 else逻辑 ,也就是在我们使用-javaagent加载agent.jar时 ,走的是这段代码:
else {
// Try to load the agent from the standard dll directory
if (os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(),
name)) {
library = os::dll_load(buffer, ebuf, sizeof ebuf);
}
if (library == NULL) { // Try the local directory
char ns[1] = {0};
//构建将要加载的 动态链接文件的名称
if (os::dll_build_name(buffer, sizeof(buffer), ns, name)) {
//根据构建后的动态链接文件名称 加载(load)动态链接文件到内存
library = os::dll_load(buffer, ebuf, sizeof ebuf);
}
if (library == NULL) {
const char *sub_msg = " on the library path, with error: ";
size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
// If we can't find the agent, exit.
vm_exit_during_initialization(buf, NULL);
FREE_C_HEAP_ARRAY(char, buf, mtThread);
}
}
}
这段代码中先是根据name去构建了动态链接文件(win中是dll,linux下是.so) 的名称,这个其实就是为什么我们传入的是instrument 而真正执行的动态链接文件是 libinstrument.so的原因。如下是构建动态连接文件的代码截图:

image.png
之后就是加载动态链接文件,然后就是寻找OnLoad也就是上边提到的find_agent_function ,最终会将找到的动态连接文件中的Agent_OnLoad方法保存到一个entry中并返回,之后就是执行动态链接库中的Agent_OnLoad方法了也即上边已经说过的代码:

image.png
到此,寻找动态链接库以及执行动态链接库中的方法就分析完了
找到libinstrument.so的真正实现InvocationAdapter.c
而实际上 libinstrument.so 这个动态链接库的实现是位于java/instrumentat/share/native/libinstrument 入口的InvocationAdapter.c 我们不妨来简单看下:

image.png
在上边的create_vm_init_agents函数中 我们查找并执行了动态链接库libinstrument.so中的Agnet_OnLoad函数,而这个函数最终会执行到InvocationAdapter.c的Agent_OnLoad中,下边是此方法的代码:
执行Agent_OnLoad函数
这个方法的注释很重要(见下边代码中的注释),这里简单翻译下
- 此方法将被命令行上的每一个 -javaagent 参数调用一次 (因为-javaagent后边可以加多个agent jar 也就是说有几个agent jar就执行此方法几次)。
- 每次调用将创建属于自己的agent和agent相关的数据
- 解析jar文件和后边的参数(我们要知道 -javaagent可以这么配:-javaagent:xxxagent.jar=option1=value1,option2=value2)
- 读取jar的配置文件MANIFEST里Premain-Class,并且把jar文件追加到agent的class path中。
代码位于: /jdk/src/share/instrument/InvocationAdapter.c
/*
* This will be called once for every -javaagent on the command line.
* Each call to Agent_OnLoad will create its own agent and agent data.
*
* The argument tail string provided to Agent_OnLoad will be of form
* [=]. The tail string is split into the jarfile and
* options components. The jarfile manifest is parsed and the value of the
* Premain-Class attribute will become the agent's premain class. The jar
* file is then added to the system class path, and if the Boot-Class-Path
* attribute is present then all relative URLs in the value are processed
* to create boot class path segments to append to the boot class path.
*/
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {
JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
jint result = JNI_OK;
JPLISAgent * agent = NULL;
//1. 创建 JPLISAgent 专门为java提供的 JVMTI agent(重要的一步)
initerror = createNewJPLISAgent(vm, &agent);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
int oldLen, newLen;
char * jarfile;
char * options;
jarAttribute* attributes;
char * premainClass;
char * agentClass;
char * bootClassPath;
/*
* Parse [=options] into jarfile and options,解析option也就是我们-javaagent:xxxagent.jar=option1=value1 中的 option1=value1参数
*/
/*
* Agent_OnLoad is specified to provide the agent options
* argument tail in modified UTF8. However for 1.5.0 this is
* actually in the platform encoding - see 5049313.
*
* Open zip/jar file and parse archive. If can't be opened or
* not a zip file return error. Also if Premain-Class attribute
* isn't present we return an error.
*/
//读取jar文件中的一些信息
attributes = readAttributes(jarfile);
//2. 寻找 jar中MANIFEST.MF 中的 Premain-Class 类
premainClass = getAttribute(attributes, "Premain-Class");
//3. 把jar文件追加到agent的class path中。
/*
* Add to the jarfile
*/
appendClassPath(agent, jarfile);
...一些校验 这里我们略过 否则太占地
}
....略
return result;
}
创建与初始化 JPLISAgent
在createNewJPLISAgent中 创建了一个 JPLISAgent (Java Programming Language Instrumentation Services Agent),并且从Vm环境中获取了 jvmtiEnv 指针,用于后续的操作,jvmtiEnv是一个很重要的指针(在JVMTI运行时,通常一个JVMTI Agent对应一个jvmtiEnv)。
我们来看下 createNewJPLISAgent 的代码:
源码在:jdk8u/jdk/src/share/instrument/JPLISAgent.c
/*
* OnLoad processing code.
*/
/*
* Creates a new JPLISAgent.
* Returns error if the agent cannot be created and initialized.
* The JPLISAgent* pointed to by agent_ptr is set to the new broker,
* or NULL if an error has occurred.
*/
JPLISInitializationError
createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {
JPLISInitializationError initerror = JPLIS_INIT_ERROR_NONE;
jvmtiEnv * jvmtienv = NULL;
jint jnierror = JNI_OK;
*agent_ptr = NULL;
//获取jvmtienv指针从vm环境 ,jvmtienv 很重要 他是个指针,通过他可以和jvm交互
jnierror = (*vm)->GetEnv( vm,
(void **) &jvmtienv,
JVMTI_VERSION_1_1);
if ( jnierror != JNI_OK ) {
initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;
} else {
//分配空间
JPLISAgent * agent = allocateJPLISAgent(jvmtienv);
if ( agent == NULL ) {
initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;
} else {
//初始化 JPLISAgent(很重要的一步)
initerror = initializeJPLISAgent( agent,
vm,
jvmtienv);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
*agent_ptr = agent;
} else {
deallocateJPLISAgent(jvmtienv, agent);
}
}
//一些异常处理 略
}
return initerror;
}
其中我们比较关注的一步就是 初始化JPLISAgent :
源码在:jdk8u/jdk/src/share/instrument/JPLISAgent.c
JPLISInitializationError
initializeJPLISAgent( JPLISAgent * agent,
JavaVM * vm,
jvmtiEnv * jvmtienv) {
jvmtiError jvmtierror = JVMTI_ERROR_NONE;
jvmtiPhase phase;
agent->mJVM = vm;
agent->mNormalEnvironment.mJVMTIEnv = jvmtienv;
agent->mNormalEnvironment.mAgent = agent;
agent->mNormalEnvironment.mIsRetransformer = JNI_FALSE;
agent->mRetransformEnvironment.mJVMTIEnv = NULL; /* NULL until needed */
agent->mRetransformEnvironment.mAgent = agent;
agent->mRetransformEnvironment.mIsRetransformer = JNI_FALSE; /* JNI_FALSE until mJVMTIEnv is set */
agent->mAgentmainCaller = NULL;
agent->mInstrumentationImpl = NULL;
agent->mPremainCaller = NULL;
agent->mTransform = NULL;
agent->mRedefineAvailable = JNI_FALSE; /* assume no for now */
agent->mRedefineAdded = JNI_FALSE;
agent->mNativeMethodPrefixAvailable = JNI_FALSE; /* assume no for now */
agent->mNativeMethodPrefixAdded = JNI_FALSE;
agent->mAgentClassName = NULL;
agent->mOptionsString = NULL;
/* make sure we can recover either handle in either direction.
* the agent has a ref to the jvmti; make it mutual
*/
jvmtierror = (*jvmtienv)->SetEnvironmentLocalStorage(
jvmtienv,
&(agent->mNormalEnvironment));
//1. 在此处监听VMInit事件!
/* now turn on the VMInit event */
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
//2. 在监听到VMinit 初始化事件后执行 eventHandlerVMInit方法的逻辑 (重要的一步)
callbacks.VMInit = &eventHandlerVMInit;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
check_phase_ret_blob(jvmtierror, JPLIS_INIT_ERROR_FAILURE);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
if ( jvmtierror == JVMTI_ERROR_NONE ) {
jvmtierror = (*jvmtienv)->SetEventNotificationMode(
jvmtienv,
JVMTI_ENABLE,
JVMTI_EVENT_VM_INIT,
NULL /* all threads */);
check_phase_ret_blob(jvmtierror, JPLIS_INIT_ERROR_FAILURE);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
return (jvmtierror == JVMTI_ERROR_NONE)? JPLIS_INIT_ERROR_NONE : JPLIS_INIT_ERROR_FAILURE;
}
初始化JPLISAgent 做了两件我们比较关注的事情,就是: 1. 监听VMinit 初始化事件 2. 在监听到VMinit事件后,设置eventHandlerVMInit回调函数。 而在这里,本质上只是设置监听的事件(VM初始化),真正触发这个事件并执行的 是在Threads::create_vm中的 post_vm_initialized,截图如下:

image.png

image.png

image.png
接下来就是通过post_vm_initialized来执行 (在initializeJPLISAgent中)提前设置好的vm初始化回调事件即:eventHandlerVMInit。
执行eventHandlerVMInit方法
eventHandlerVMInit方法比较重要,紧接着我们来看下:
源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c


















