说明: 本文很长,长到超出了掘金编辑器的限制字符数 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