前言
之前写了一篇关于链路追踪的文章: 自实现分布式链路追踪 方案&实践 ,期间提到过可以使用ttl(全称:transmittable-thread-local) 去解决线程间链路id丢失的问题,而其中无侵入的使用方式:agent 插桩技术深深的吸引了我,谁曾想一入 agent 深似海,我差点出不来了😂😂😂。
说明: 本文很长,长到超出了掘金编辑器的限制字符数 10万,所以我在最后边只是图解,没有更多的文字和代码描述了,本文知识点较多,如果没接触过agent那必然大概率会懵(大部分知识点讲解完后,我都会配个图来总结归纳加强理解
)。当你一点点去理解尝试后相信会有所收获,另外水平有限不对地方请指导。
本文大概结构:
- 前置节:
简单认识 ->
JVMTI,Java Agent,JVMTIAgent,libinstrument.so (先混个脸熟) - 第一节:
Java Agent
介绍与(静/动)态加载方式描述+图解。 - 第二节:
JVMTI
介绍,功能&使用场景以及c语言自实现一个JVMTIAgent - 第三节:Java Agent
静态加载
demo+源码分析+图解 - 第四节:Java Agent
动态加载
demo+源码分析+图解 - 第五节:自言自语😂😂
本文涉及到的知识点:
- 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
这个文件中,截图如下: -
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
三个我们比较关注的函数,截图如下: 当我们静态加载agent jar(启动时添加vm参数 -javaagent:xxxjar包路径的方式)时Agent_OnLoad
会调用到我们的premain
方法,当我们动态加载(JVM的attach机制,通过发送load命令来加载)时Agent_OnAttach
会调用到我们的agentmian
方法。也许你现在不明白这个,但是当你看了下边的第三&四节源码后你就能串起来了。 -
Instrumentation API:
为Java Agent提供了一套Java层面的接口
,它是在Java 5开始引入的,旨在为Java开发者提供一种标准方式来动态修改类的行为以及做增强操作,部分示例: -
JVMTIAgent:
是一个动态链接库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:- Agent_OnLoad函数: 会在静态加载agent jar时调用。
- Agent_OnAttach函数: 如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用该函数。
- Agent_OnUnload函数: 在agent卸载时调用。
- 更强大的功能: 在我们使用Java Agent时不管是静态加载还是动态加载,其实实现的功能比较有限,基本上也就是下边这些:
静态加载可以实现
:类加载时修改(transform)/(redefine)重定义类字节码动态加载可以实现
:运行时修改类字节码,dump线程堆栈信息,获取系统配置等。动态加载实现的功能 完整的无非就是下边这几个:
- 而如果你直接去使用c编写一个JVMTIAgent, 那能实现的功能就比较多了,你可以根据需要实现JVMTI预留出的每一个钩子函数,从而在指定的时机来让jvm加载你的逻辑以达到你的目的,这就是钩子函数的灵活之处。
以上几个知识点之间的关系图如下: ps: (牢记这几个知识点之间的关系与各自的功能,会使我们理解本文起到事半功倍的效果!!! )
接下来,我们深入展开讲解下以上这些知识点。
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。具体步骤如下:
- 准备Agent JAR : 与静态加载相同,需要准备一个包含
agentmain
方法的Agent JAR文件。agentmain
方法是动态加载Agent时由JVM调用的入口点。该JAR文件还需要在其MANIFEST.MF
中声明Agent-Class
属性,指向包含agentmain
方法的类。编写转换增强(使用字节码工具比如javaassist 或ASM )逻辑 需要实现ClassFileTransformer
类的transform方法,与静态加载不同,此方法的调用需要通过inst.retransformClasses("要重新加载的类");
来触发。- 使用Attach API : Attach API允许一个运行中的Java进程连接(通过UNIX套接字)到另一个Java进程。一旦连接,它可以用来加载Agent JAR。这通常通过使用
com.sun.tools.attach.VirtualMachine
类实现,该类提供了附加到目标JVM进程并加载Agent的方法- 加载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,以便后边引用 ):
c
源码在: /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 的主要功能&使用场景
功能:
- 事件通知:JVMTI允许工具通过事件获取JVM内发生的特定情况的通知,如线程启动/结束、类加载/卸载、方法进入/退出等。
- 线程管理:它提供了监控和管理Java程序中线程状态的能力。
- 堆和垃圾回收:JVMTI支持查询堆信息、监控垃圾回收事件,以及在某些条件下控制垃圾回收的执行。
- 调试支持:JVMTI为调试器提供了丰富的接口,支持断点、单步执行、字段访问/修改等调试功能。
- 性能监测:提供了监视和分析JVM性能的工具,如获取方法执行时间、内存使用情况等。
场景:
- 开发调试工具:利用JVMTI提供的调试支持,开发强大的调试工具,比如 idea ,eclipse等等。
- 性能分析:构建性能分析工具来识别Java应用的性能瓶颈。
- 监控工具:创建监控工具来实时监视JVM的健康状况和性能指标。
- 覆盖率分析:通过跟踪类和方法的加载与执行,帮助生成代码覆盖率报告
文字描述你可能感觉不到什么,但是如果提到这些框架,你大概率会知晓其中的一个或者几个,而他们就是基于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,如下:这个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 这个文件中,如下: 可以看到有很多回调钩子(本文所讲的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代码实现, 如果是Java 5之前?对不起,你只能是通过原生来实现也就是c/c++代码。
我们下边就给他使用c代码实现一个 JVMTI中 ClassFileLoadHook, 这个钩子函数中的逻辑比较简单,它演示了如何使用c语言设置ClassFileLoadHook
事件回调,并在回调函数中简单地打印被加载的类的名称(注意: 此处小案例使用了启动时静态加载
,如果要动态加载需要实现 Agent_OnAttach函数,这里我们不做演示)。步骤如下:
1. 创建JVMTI Agent:
创建一个名为
ClassFileLoadHookAgent.c
的C文件,用于实现JVMTI Agent:
c#include <jvmti.h> #include <stdio.h> #include <stdlib.h> // 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命令来进行编译:
bashgcc -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文件,如下:
bashjava -agentpath:/usr/local/src/agent/classfileloadhookagent.so NativeCodeImplClassFileLoadHookTest
当Java应用程序运行时,每当类文件被加载前,你的
ClassFileLoadHook
回调函数将被触发,打印出即将加载的类的名称,接下来我们实操&演示下。实操&演示
下面进行演示,如下:
(注意代码中是去掉包名的因为这样我们只需要
java NativeCodeImplClassFileLoadHookTest
就可以执行class文件了,有包名的话还得全限定所以我们就不加包名了)可以看到通过在 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方法一览:
通过Instrument API和javaassist 编写插桩代码:
关于javaassist如果不了解的话,可以参考我的上一篇文章:Javassist使用教程【译】
java
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代码如下:
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<!-- Maven Assembly Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<!-- 将所有的依赖全部打包进jar -->
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<!-- MainClass in mainfest make a executable jar -->
<archive>
<manifestEntries>
<!--设置jar的作者和时间-->
<Built-By>黄壮壮</Built-By>
<Built-Date>${maven.build.timestamp}</Built-Date>
<!--指定premain方法(静态加载时会调用的方法)的入口类,也就是说告诉jvm, premain方法在哪个类中-->
<Premain-Class>com.xzll.agent.config.JdkDateAgentTest</Premain-Class>
<!--该属性设置为 true 时表示:允许已加载的类被重新转换(retransform)。这意味着 Java Agent 可以在运行时修改已经加载的类的字节码,而不需要重新启动应用或 JVM
注意,如果此属性设置为 false 在执行main方法且设置-jaavaagent.jar时,将会执行抛出异常 :java.lang.instrument ASSERTION FAILED ***: "result" with message agent load/premain call failed at src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 422
-->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<!--该属性设置为 true 时表示:允许 Java Agent 在运行时重新定义(也就是完全替换)已加载的类的字节码,这里我们没用到这个暂时设置成false,用到时在打开-->
<Can-Redefine-Classes>false</Can-Redefine-Classes>
<!--该属性设置为 true 时表示:允许 Java Agent 在运行时动态地为 JNI (Java Native Interface) 方法设置前缀。这项能力主要用于修改或拦截对本地方法的调用,这里我们没用到也设置为false -->
<Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>
<!--指定agentmain方法的入口类(动态加载时将会调用 agentmain方法)-->
<!--<Agent-Class>com.xzll.agent.config.MysqlFieldCryptByExecuteBodyAgent</Agent-Class>-->
</manifestEntries>
<!--如果不在pom中设置以上manifestEntries 这些信息,那么也可以在手动建一个MANIFEST.MF文件在 src/main/resources/META-INF/目录中,并将这些信息手动写进文件,然后让assembly打包时使用我们自己手写的这个MANIFEST.MF文件(如下的 manifestFile 标签就是告诉插件使用我们自己写的MANIFEST.MF文件),但是那样容易出错所以我们最好是在pom中设置然后让assembly插件帮我们生成 -->
<!--<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>-->
</archive>
</configuration>
<executions>
<!-- 配置执行器 -->
<execution>
<id>make-assembly</id>
<!-- 绑定到package命令的生命周期上 -->
<phase>package</phase>
<goals>
<!-- 只运行一次 -->
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
使用mvn package 命令打包
解压jar 并查看/META-INF/MANIFEST.MF文件内容
使用命令解压jar:
bash
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文件内容:
编写&执行main方法(使用-javaagent静态加载上边的agent jar包)
编写并执行main方法,这里我们很重要的一步就是在 vm参数中配置了 此内容:
bash
-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
也就是我们所说的: 静态加载 。 看下效果: 可以看到,在main方法启动时添vm参数(即:
bash
-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 方法体内容是:
java
{
long currentTime = getTimeImpl();
System.out.println(" 使用agent探针对Date方法进行修改并打印,当前时间【毫秒级】:"+currentTime );
return currentTime/1000;
}
,因此让Date getTime()方法返回了秒级时间戳。 ,这就是所谓的 插桩。是不是有点aop的意思?
以上就是静态加载的demo了,虽然很简单,但是麻雀虽小五脏俱全了也算是,趁热打铁吧
,下边我们就从 源码角度来逐步分析静态加载实现的流程与原理
,注意 源码小节比较重要
,看完源码,才会有恍然大悟的感觉。没错我就是这个感觉。
静态加载源码解析
解析启动时传入的vm参数
源码这一节我准备从源头说起,我们知道静态加载agent时我们必须使用-javaagent:xxx.jar 而我们就从这里说起,看看jvm到底是如何解析运作的,首先第一步传入的参数jvm得认识吧?所以就来到了 解析参数这一步,解析参数的入口在这里: 接下来到 parse_each_vm_init_arg 这个里边,而这个函数的内容超级多,因为我们知道vm参数巨多,所以这个里边的代码也巨长,但是我们这里只关心-javaagent,其他的我们知道了解即可,
完整代码在: /hotspot/src/share/vm/runtime/arguments.cpp 中
c++
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 中
c++
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 中
c++
// 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 中
c++
// 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
, 而这个参数就是 AgentLibrary的 is_absolute_path
,所以根据这里我们可以得出 当使用-javaagent这种方式静态加载Java Agent时 走的是lookup_on_load方法的 else逻辑 ,也就是在我们使用-javaagent加载agent.jar时 ,走的是这段代码:
c++
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的原因。如下是构建动态连接文件的代码截图:
之后就是加载动态链接文件,然后就是寻找OnLoad也就是上边提到的find_agent_function ,最终会将找到的动态连接文件中的Agent_OnLoad方法保存到一个entry中并返回,之后就是执行动态链接库中的Agent_OnLoad方法了也即上边已经说过的代码: 到此,寻找动态链接库以及执行动态链接库中的方法就分析完了
找到libinstrument.so的真正实现InvocationAdapter.c
而实际上 libinstrument.so 这个动态链接库的实现是位于java/instrumentat/share/native/libinstrument 入口的InvocationAdapter.c 我们不妨来简单看下:
在上边的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中。
c++
代码位于: /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
* <jarfile>[=<options>]. 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 <jarfile>[=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
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
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 做了两件我们比较关注的事情,就是:
- 监听VMinit 初始化事件
- 在监听到VMinit事件后,设置eventHandlerVMInit回调函数。 而在这里,本质上只是设置监听的事件(VM初始化),真正触发这个事件并执行的 是在
Threads::create_vm
中的post_vm_initialized
,截图如下:
接下来就是通过post_vm_initialized来执行 (在initializeJPLISAgent中)提前设置好的vm初始化回调事件即:eventHandlerVMInit
。
执行eventHandlerVMInit方法
eventHandlerVMInit方法比较重要,紧接着我们来看下:
源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c
c++
/*
* JVMTI callback support
*
* We have two "stages" of callback support.
* At OnLoad time, we install a VMInit handler.
* When the VMInit handler runs, we remove the VMInit handler and install a
* ClassFileLoadHook handler.
*/
void JNICALL
eventHandlerVMInit( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jthread thread) {
JPLISEnvironment * environment = NULL;
jboolean success = JNI_FALSE;
// 从jvmtienv 中获取JPLISAgent的环境
environment = getJPLISEnvironment(jvmtienv);
/* process the premain calls on the all the JPL agents */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
//执行processJavaStart 开始
success = processJavaStart( environment->mAgent,
jnienv);
restoreThrowable(jnienv, outstandingException);
}
/* if we fail to start cleanly, bring down the JVM */
if ( !success ) {
abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART);
}
}
执行processJavaStart函数
eventHandlerVMInit
中的processJavaStart
,从名字上来看也很明了就是启动Java相关的程序。接下来我们会发现 越看越离java近。processJavaStart代码如下:
源码在:/jdk8u/jdk/src/share/instrument/JPLISAgent.c
c++
/*
* VMInit processing code.
*/
/*
* If this call fails, the JVM launch will ultimately be aborted,
* so we don't have to be super-careful to clean up in partial failure
* cases.
*/
jboolean
processJavaStart( JPLISAgent * agent,
JNIEnv * jnienv) {
jboolean result;
/*
* OK, Java is up now. We can start everything that needs Java.
* ok , Java 现在已经启动了。我们可以开始运行所有需要 Java 的应用程序了。
*/
/*
* First make our emergency fallback InternalError throwable.
*/
result = initializeFallbackError(jnienv);
jplis_assert(result);
/*
* Now make the InstrumentationImpl instance.
*
* 现在创建 InstrumentationImpl的实例,在这里我们知道:
* InstrumentationImpl的实例不是在Java中new 的,而是由jvm创建的,通过premain方法传给java然后就可以使用了。
*/
if ( result ) {
result = createInstrumentationImpl(jnienv, agent);
jplis_assert(result);
}
/*
* Then turn off the VMInit handler and turn on the ClassFileLoadHook.
* This way it is on before anyone registers a transformer.
*
* 在此方法中注册类加载时的回调函数 (ClassFileLoadHook),
* 对应的最终实现就是 ClassFileTransformer的 transform
*/
if ( result ) {
result = setLivePhaseEventHandlers(agent);
jplis_assert(result);
}
/*
* Load the Java agent, and call the premain.
* 加载java agent并调用premain方法,看到没这就是调用premain方法的地方!
*/
if ( result ) {
result = startJavaAgent(agent, jnienv,
agent->mAgentClassName, agent->mOptionsString,
agent->mPremainCaller);
}
/*
* Finally surrender all of the tracking data that we don't need any more.
* If something is wrong, skip it, we will be aborting the JVM anyway.
*/
if ( result ) {
deallocateCommandLineData(agent);
}
return result;
}
通过阅读processJavaStart
代码,我们知道这里边首先
- 创建 (sun.instrument.InstrumentationImpl)类的实例
- 监听&开启 ClassFileLoadHook 事件,注册回调函数最终此回调函数会调用到:ClassFileTransformer的 transform 。
- 加载java agent并调用premain方法(会把Instrumentation类实例和agent参数传入premain方法中去),premain中会将ClassFileTransformer的的实现添加进 Instrumentation类的实例中去
开启并监听ClassFileLoadHook事件 -> setLivePhaseEventHandlers
而其中的第二步即 :监听&开启 ClassFileLoadHook 事件,里边的操作比较重要我们要知道,所以下边看下源码:
源码在:/jdk8u/jdk/src/share/instrument/JPLISAgent.c
c++
jboolean
setLivePhaseEventHandlers( JPLISAgent * agent) {
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
/* first swap out the handlers (switch from the VMInit handler, which we do not need,
* to the ClassFileLoadHook handler, which is what the agents need from now on)
*/
memset(&callbacks, 0, sizeof(callbacks));
//设置回调事件处理器
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
check_phase_ret_false(jvmtierror);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
if ( jvmtierror == JVMTI_ERROR_NONE ) {
/* turn off VMInit */
jvmtierror = (*jvmtienv)->SetEventNotificationMode(
jvmtienv,
JVMTI_DISABLE,
JVMTI_EVENT_VM_INIT,
NULL /* all threads */);
check_phase_ret_false(jvmtierror);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
if ( jvmtierror == JVMTI_ERROR_NONE ) {
/* turn on ClassFileLoadHook */
//启用ClassFileLoadHook事件
jvmtierror = (*jvmtienv)->SetEventNotificationMode(
jvmtienv,
JVMTI_ENABLE,
JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
NULL /* all threads */);
check_phase_ret_false(jvmtierror);
jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
}
return (jvmtierror == JVMTI_ERROR_NONE);
}
上边这个函数中会设置 ClassFileLoadHook 的处理器,即类加载时的回调处理器 eventHandlerClassFileLoadHook
但是有一点我们要清楚,这里只是设置回调函数,并没有真正执行eventHandlerClassFileLoadHook的内容,因为此时还不到类加载阶段,切记这一点
在这个eventHandlerClassFileLoadHook里边会最终调用(注意不是此时调用,而是类加载时 )到我们的 jdk中的ClassFileTransformer
接口的transform
方法,接下来我们看下:
设置类加载时的回调函数处理器:eventHandlerClassFileLoadHook
源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c
c++
void JNICALL
eventHandlerClassFileLoadHook( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protectionDomain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data) {
JPLISEnvironment * environment = NULL;
environment = getJPLISEnvironment(jvmtienv);
/* if something is internally inconsistent (no agent), just silently return without touching the buffer */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
transformClassFile( environment->mAgent,
jnienv,
loader,
name,
class_being_redefined,
protectionDomain,
class_data_len,
class_data,
new_class_data_len,
new_class_data,
environment->mIsRetransformer);
restoreThrowable(jnienv, outstandingException);
}
}
上边这个 eventHandlerClassFileLoadHook方法就是监听到类加载时的处理逻辑。其中的transformClassFile
会执行到我们的java代码,见下边:
调用到java代码的地方 -> transformClassFile
源码在: /jdk8u/jdk/src/share/instrument/JPLISAgent.c
c++
/*
* Support for the JVMTI callbacks
*/
void
transformClassFile( JPLISAgent * agent,
JNIEnv * jnienv,
jobject loaderObject,
const char* name,
jclass classBeingRedefined,
jobject protectionDomain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data,
jboolean is_retransformer) {
jboolean errorOutstanding = JNI_FALSE;
jstring classNameStringObject = NULL;
jarray classFileBufferObject = NULL;
jarray transformedBufferObject = NULL;
jsize transformedBufferSize = 0;
unsigned char * resultBuffer = NULL;
jboolean shouldRun = JNI_FALSE;
//.........略过n多行代码.........
/* now call the JPL agents to do the transforming */
/* potential future optimization: may want to skip this if there are none */
//!!!!!!! 这一步相当重要,他就是调用到我们java代码(`InstrumentationImpl`类的`transform`方法)
//的地方
if ( !errorOutstanding ) {
jplis_assert(agent->mInstrumentationImpl != NULL);
jplis_assert(agent->mTransform != NULL);
//调用jdk中InstrumentationImpl类的的transform方法
transformedBufferObject = (*jnienv)->CallObjectMethod(
jnienv,
agent->mInstrumentationImpl,
agent->mTransform,
loaderObject,
classNameStringObject,
classBeingRedefined,
protectionDomain,
classFileBufferObject,
is_retransformer);
errorOutstanding = checkForAndClearThrowable(jnienv);
jplis_assert_msg(!errorOutstanding, "transform method call failed");
}
//.........略过n多行代码.........
}
return;
}
找到将被调用(注意不是此时调用)的java代码!!!(InstrumentationImpl类的transform方法)
而上边这个(transformedBufferObject = (*jnienv)->CallObjectMethod(n多个参数)
)这段代码最终就会调到jdk中InstrumentationImpl
类的的transform
方法,如下: 我去,终于看到自己写的代码了,不容易啊。翻山越岭的。
在开启监听类加载事件 并 注册完类加载时的回调函数后,进行下边逻辑
加载java agent并调用premain方法------> startJavaAgent
调用我们MAINFEST.MF
Premain-Class
类中的premain
方法并传入参数(包括启动时-javaagent:xxjava.jar=option1=value1=option2=value2
传入的参数和Instrumentation
的实例对象)
调用到jdk代码-> sun.instrument.InstrumentationImpl的loadClassAndCallPremain
注意:loadClassAndCallPremain
中会调用loadClassAndStartAgent
方法
java代码如下:
java
代码在 sun.instrument.InstrumentationImpl类中
/**
* 静态加载时 被jvm直接调用的是loadClassAndCallPremain这个方法
*
*/
// WARNING: the native code knows the name & signature of this method
private void
loadClassAndCallPremain( String classname,
String optionsString)
throws Throwable {
//静态加载调用的最终方法名: premain
loadClassAndStartAgent( classname, "premain", optionsString );
}
// Attempt to load and start an agent
//从这里启动并加载一个agent
private void
loadClassAndStartAgent( String classname,
String methodname,
String optionsString)
throws Throwable {
ClassLoader mainAppLoader = ClassLoader.getSystemClassLoader();
Class<?> javaAgentClass = mainAppLoader.loadClass(classname);
Method m = null;
NoSuchMethodException firstExc = null;
boolean twoArgAgent = false;
// The agent class must have a premain or agentmain method that
// has 1 or 2 arguments. We check in the following order:
//
// 1) declared with a signature of (String, Instrumentation)
// 2) declared with a signature of (String)
// 3) inherited with a signature of (String, Instrumentation)
// 4) inherited with a signature of (String)
//
// So the declared version of either 1-arg or 2-arg always takes
// primary precedence over an inherited version. After that, the
// 2-arg version takes precedence over the 1-arg version.
//
// If no method is found then we throw the NoSuchMethodException
// from the first attempt so that the exception text indicates
// the lookup failed for the 2-arg method (same as JDK5.0).
try {
m = javaAgentClass.getDeclaredMethod( methodname,
new Class<?>[] {
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// remember the NoSuchMethodException
firstExc = x;
}
if (m == null) {
// now try the declared 1-arg method
try {
m = javaAgentClass.getDeclaredMethod(methodname,
new Class<?>[] { String.class });
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// two arg inheritance next
}
}
if (m == null) {
// now try the inherited 2-arg method
try {
m = javaAgentClass.getMethod( methodname,
new Class<?>[] {
String.class,
java.lang.instrument.Instrumentation.class
}
);
twoArgAgent = true;
} catch (NoSuchMethodException x) {
// ignore this exception because we'll try
// one arg inheritance next
}
}
if (m == null) {
// finally try the inherited 1-arg method
try {
m = javaAgentClass.getMethod(methodname,
new Class<?>[] { String.class });
} catch (NoSuchMethodException x) {
// none of the methods exists so we throw the
// first NoSuchMethodException as per 5.0
throw firstExc;
}
}
// the premain method should not be required to be public,
// make it accessible so we can call it
// Note: The spec says the following:
// The agent class must implement a public static premain method...
setAccessible(m, true);
//通过反射执行传入的方法名称premian(静态加载时传的是premain,动态加载传的是agentmain),
//即:我们在MAINFEST.MF中指定的Premain-Class类里边的premain方法
// invoke the 1 or 2-arg method
if (twoArgAgent) {
m.invoke(null, new Object[] { optionsString, this });
} else {
m.invoke(null, new Object[] { optionsString });
}
}
调用到我们MAINFEST.MF
文件中-> Premain-Class
类中的premain
方法(我们自己开发的代码)
loadClassAndStartAgent
最终会通过反射执行我们在MAINFEST.MF中指定的Premain-Class类里边的premain方法,值的注意的是:在premain方法中其实只是往 InstrumentationImpl
实例中添加了我们自己定义的类转换器(比如我的DefineTransformer类),还没有真正的执行DefineTransformer
的transform
函数
以下是我的premain方法:
那么什么时候会执行(或者说 回调 ,这个词更符合此函数的调用动作)到我的DefineTransformer
类中的tranform
方法去修改(Retransform)
或者 重新定义(Redefine)
类呢?那肯定是类加载时啊,上边我们说过很多遍了!
加载类的入口: systemDictionary.cpp-> load_instance_class
而jvm中类加载是从这个地方开始的(systemDictionary.cpp): 因为我们自己编写的类都是要通过系统类加载器加载的,所以会走到这个系统类加载,我们继续跟,来到classLoader.cpp中的 load_calassfile方法,如下:
类加载时回调
在premain中设置的转换器,此处的话就是: DefineTransformer
类的transform
方法
注意我们本文中的 DefineTransformer 类实现了
java.lang.instrument.ClassFileTransformer
接口的transform
方法!所以才会调用到DefineTransformer
类的transform
方法!这一点要明白!
继续跟进load_calassfile中的 parseClassFile方法: ps: 这个方法巨长,至少有600多行,类加载的主要逻辑就在这里边了,感兴趣可以去看看完整的,这里我们不粘完整版本了,只保留我们感兴趣的,调用类加载时候的钩子函数片段,代码如下:
c++
源码在: hotspot/src/share/vm/classfile/classFileParser.cpp 中
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
ClassLoaderData* loader_data,
Handle protection_domain,
KlassHandle host_klass,
GrowableArray<Handle>* cp_patches,
TempNewSymbol& parsed_name,
bool verify,
TRAPS) {
// When a retransformable agent is attached, JVMTI caches the
// class bytes that existed before the first retransformation.
// If RedefineClasses() was used before the retransformable
// agent attached, then the cached class bytes may not be the
// original class bytes.
JvmtiCachedClassFileData *cached_class_file = NULL;
Handle class_loader(THREAD, loader_data->class_loader());
bool has_default_methods = false;
bool declares_default_methods = false;
// JDK-8252904:
// The stream (resource) attached to the instance klass may
// be reallocated by this method. When JFR is included the
// stream may need to survive beyond the end of the call. So,
// the caller is expected to declare the ResourceMark that
// determines the lifetime of resources allocated under this
// call.
ClassFileStream* cfs = stream();
// Timing
assert(THREAD->is_Java_thread(), "must be a JavaThread");
JavaThread* jt = (JavaThread*) THREAD;
init_parsed_class_attributes(loader_data);
if (JvmtiExport::should_post_class_file_load_hook()) {
JvmtiThreadState *state = jt->jvmti_thread_state();
if (state != NULL) {
KlassHandle *h_class_being_redefined =
state->get_class_being_redefined();
if (h_class_being_redefined != NULL) {
instanceKlassHandle ikh_class_being_redefined =
instanceKlassHandle(THREAD, (*h_class_being_redefined)());
cached_class_file = ikh_class_being_redefined->get_cached_class_file();
}
}
unsigned char* ptr = cfs->buffer();
unsigned char* end_ptr = cfs->buffer() + cfs->length();
//在此处回调我们设置的 回调函数,本文是: DefineTransformer类的transform函数
JvmtiExport::post_class_file_load_hook(name, class_loader(), protection_domain,
&ptr, &end_ptr, &cached_class_file);
if (ptr != cfs->buffer()) {
// JVMTI agent has modified class file data.
// Set new class file stream using JVMTI agent modified
// class file data.
cfs = new ClassFileStream(ptr, end_ptr - ptr, cfs->source());
set_stream(cfs);
}
}
//........... 此处略去至少 400 ~ 500 行代码 ,想目睹类加载详情的,建议看看。很精彩 ...........
// Clear class if no error has occurred so destructor doesn't deallocate it
_klass = NULL;
return this_klass;
}
jvmtiExport.cpp -> post_class_file_load_hook: jvmtiExport.cpp -> post_all_envs: jvmtiExport.cpp -> post_all_envs中的 post_to_env : 上边方法post_to_env中的这段:
c++
jvmtiEventClassFileLoadHook callback = env->callbacks()->ClassFileLoadHook;
if (callback != NULL) {
(*callback)(env->jvmti_external(), jni_env,
jem.class_being_redefined(),
jem.jloader(), jem.class_name(),
jem.protection_domain(),
_curr_len, _curr_data,
&new_len, &new_data);
}
首先会直接调用InstrumentationImpl
中的transform
,之后此方法会间接调用到我们编写的DefineTransformer(实现了ClassFileTransformer接口的transform)类的transform方法!!! 我的类 增强or修改 方法如下:
将修改后的字节码保存到类文件流中去
在调用完DefineTransformer类的transform方法后,从上边可以看到返回了修改后的字节码,需要将修改后的类数据添加到类文件流,使得修改后的内容生效呀(最终加载到元空间的是我们在DefineTransformer类transform方法 修改后的内容),所以就有了下边的代码:
执行加载
后边的逻辑: -> 链接
(验证,准备,解析)-> 初始化
-> 使用
(如new or 反射 等等)
在 初始化这一步之后,类的元数据
被保存到了元空间(1.8后引入的)中,之后我们就可以愉快的使用了,比如new 或者反射等等根据类元数据创建实例这类行为,或者访问类的元数据比如 类.class 等等操作。
到此,就算真正的将静态加载jar以及插桩是如何执行的这些流程串联起来了。真不容易。我都不知道我怎么坚持下来的😄 整个流程比较复杂,观看代码太枯燥,还是画个图吧,更直观(一图胜千言!) 如下:
静态加载图解(重要,重要,重要!)
上图简单语言概括下:
-
【通过main函数启动java程序】
-
【cerate_vm开始】
- 2.1、
注册
虚拟机初始化时 (对应事件类型是 VMInit) 事件发生时的回调函数:eventHandlerVMinit - 2.2、vm初始化开始,回调步骤2.1 设置的回调函数:eventHandlerVMinit
-
2.2.1、
注册
类加载时(对应事件类型是 ClassFileLoadHook )的回调函数为InstrumentationImpl
的transform (最终的实现是ClassFileTransformer
接口的实现类里的transform方法) -
2.2.2、直接
执行
的是InstrumentationImpl的loadClassAndStartAgent
方法,最终调用到agent中的Premain-Class中的premain方法(该方法是往Instrumentation
实例中设置了类转换器,并没有真正执行类转换的操作)
-
- 2.1、
-
【create_vm函数执行完毕,开始类加载工作】
-
3.1、加载
-
3.1.1、
回调
步骤2.2.1 中设置的类加载时的回调函数:InstrumentationImpl的transform(最终会调用到
实现了ClassFileTransformer
接口的实现类里的transform方法),进行类的增强 or 修改等操作,并返回修改后的字节码。 -
3.1.2、将修改后的字节码生效。即保存到类数据文件流中。
-
-
3.2、后续操作: -> 链接(验证、准备、解析)-> 初始化-> 使用
-
到此,你清楚静态加载时Java Agent的工作原理和机制了吗???ok接下来我们说说动态加载。
4、Java Agent 动态加载
演示、图解、源码分析
动态加载相较于静态加载,会更灵活一点,我们演示下如何实现一个动态加载的agent。
动态加载demo实现与演示
想要达到的效果(让Integer.valueOf(int i)每次都装箱,不从IntegerCache数组中取,也就是要达到-127-128两个Integer对象之间的对比也会返回false)
我们知道如果你声明了两个局部变量:( Integer i1=20;
和Integer i2=20;
),编译为class后将会被Integer.valueOf(int i);
方法包装(可以参考我之前的文章:Integer缓存那点小事儿),去==
比较时会返回true,这个原因是因为当i 在-128-127范围内时,valueOf不会将i装箱,而是从缓存数组中取对应索引的Integer对象,相同的值取得是相同索引位置的对象 ==比较时自然是相等,而我们此处的案例想要的目的是:i1和i2去==比较时是不相等的,想要达到这个目的就得修改Integer.valueOf(int i);
方法的实现,将-128-127的int值都装箱,这样的话 只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象
修改前
的 Integer.value(int i); 代码:
java
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
修改后
的 Integer.value(int i); 代码:
arduino
public static Integer valueOf(int i) {
return new Integer(i);
}
修改jdk代码肯定是行不通,人家也不让你直接修改,我们这里准备通过agent修改,然后动态加载agent jar ,是不是很熟悉?没错 热部署 就是类似的原理。即不用重启即让代码生效。
编写agent jar的逻辑实现
基本上编写一个agent jar需要三个内容
编写agentmain方法(即加载agent的入口)
编写transform类转换方法(即对类/方法/字段进行字节码修改的地方)
截图放不下,直接贴代码:
java
/**
* @Author: 黄壮壮
* @Date: 2024/4/23 10:37:11
* @Description:
*/
public class AttachAgent {
static class ByAttachLoadAgentTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//操作Integer类
if ("java/lang/Integer".equals(className)) {
CtClass clazz = null;
System.out.println("动态加载agent jar -> 动态插桩Integer.valueOf方法【开始】," + "当前类:" + className);
try {
// 从ClassPool获得CtClass对象 (ClassPool对象是CtClass对象的容器,CtClass对象是类文件的抽象表示)
final ClassPool classPool = ClassPool.getDefault();
//不配这个 将找不到 java.lang.Integer 类
classPool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
clazz = classPool.get("java.lang.Integer");
//获取到Integer的valueOf(int i) 方法。注意此处需要指定形参是int的 因为有多个valueOf方法
CtMethod valueOf = clazz.getDeclaredMethod("valueOf", new CtClass[]{CtClass.intType});
//(修改字节码) 这里对 java.lang.Integer的valueOf(int i)进行改写,将以下代码:
/**
* public static Integer valueOf(int i) {
* if (i >= IntegerCache.low && i <= IntegerCache.high)
* return IntegerCache.cache[i + (-IntegerCache.low)];
* return new Integer(i);
* }
*
* 改为:
* public static Integer valueOf(int i) {
* System.out.println("修改valueOf方法的实现,将-128-127的int值都装箱,这样的话 只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象");
* return new Integer(i);
* }
*/
//在此处修改valueOf方法的实现,将-128-127的int值都装箱,这样的话只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象
String methodBody = "{" +
"return new Integer($1);" +
"}";
valueOf.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("动态加载agent jar -> 动态插桩Integer.valueOf方法【结束】," + "当前类:" + className);
}
}
return classfileBuffer;
}
}
}
编写maven的一些属性,让其生成MAINFEST.MF文件(用到哪些就开启哪些,不用的最好关掉)
打jar包 并检查 META-INF/MANIFEST.MF中的内容
打包并查看xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies/META-INF/MANIFEST.MF中的内容,如下:
编写目标程序,即:(将要被attach的java程序)
编写发起attach的程序 即:(请求jvm 动态加载agent jar)
java
/**
* @Author: 黄壮壮
* @Date: 2023/3/3 09:15:21
* @Description:
*/
public class AttachAgentTest {
public static void main(String[] args)throws Exception {
//1. 根据进程id 与目标jvm程序建立 socket连接
VirtualMachine vm = VirtualMachine.attach("目标程序的pid");
try {
//2. 加载指定的 agent jar,本质是发送请求
vm.loadAgent("/usr/local/src/agent/attach/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
// vm.loadAgent("/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
} finally {
//程序结束时 卸载agent jar
// vm.detach();
}
Thread.sleep(20000);
}
}
演示效果
接下来将这三个文件上传到我的linux服务器:
javac 编译并运行 AttachTarget
将AttachTarget的pid输入到attach发起程序AttachAgentTest中,编译并运行 AttachAgnetTest
最终动态加载agent jar的效果:
可以看到,通过动态加载成功插桩到java.lang.Integer的valueOf方法实现了运行时修改类无需重启的效果。此处只是个小案例,如果配合ASM(更强大功能齐全的字节码操作工具)或者其他技术, 可能还会发挥出更强大的功能,但是要走正路,不能搞破坏,哈哈~。
动态加载源码解析
上边简单演示了下动态加载的使用,下边我们还是称热打铁从源码角度分析
发起attach的源码分析,这要从java代码-> 【VirtualMachine.attach("目标程序的pid")
】开始看起:
com.sun.tools.attach.VirtualMachine类的attach方法:
java
public static VirtualMachine attach(String id)
throws AttachNotSupportedException, IOException
{
if (id == null) {
throw new NullPointerException("id cannot be null");
}
List<AttachProvider> providers = AttachProvider.providers();
if (providers.size() == 0) {
throw new AttachNotSupportedException("no providers installed");
}
AttachNotSupportedException lastExc = null;
for (AttachProvider provider: providers) {
try {
//开始attach
return provider.attachVirtualMachine(id);
} catch (AttachNotSupportedException x) {
lastExc = x;
}
}
throw lastExc;
}
之后会进入 provider.attachVirtualMachine(id);
这个逻辑里边,
而这个AttachProvider是一个抽象类,想要实现attach必须间接实现
此类的attachVirtualMachine 方法(需要直接实现HotSpotVirtualMachine
类),不同系统有不同的实现,如下: 其实最好是以linux为例(因为我们的demo就是在linux系统上演示的),源码: 但是因为我安装的是mac版本的jdk,所以我的java源码中的实现只有bsd系统(mac底层是基于bsd)的实现,即: BsdAttachProvider ,所以这里我们以bsd系统的 BsdAttachProvider 为例(值的注意的是BsdAttachProvider 和 LinuxAttachProvider 基本上一致 所以这里也就不纠结非得看LinuxAttachProvider的源码了):
类 sun.tools.attach.BsdAttachProvider
的attachVirtualMachine
方法如下:
java
public VirtualMachine attachVirtualMachine(String var1) throws AttachNotSupportedException, IOException {
this.checkAttachPermission();
this.testAttachable(var1);
return new BsdVirtualMachine(this, var1);
}
类: sun.tools.attach.BsdVirtualMachine
的构造方法
:
java
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");
}
//查找socket描述符 即/tmp/.java_pid+pid文件
this.path = this.findSocketFile(var3);
///tmp/.java_pid+pid 文件为空的话 代表目标attach程序还没创建过 tmp/.java_pid+pid文件
//也就是没启动和初始化 Attach listener线程
if (this.path == null) {
//创建/tmp//attach_pid+pid文件在宿主机,这个文件是 目标attach进程 判断收到的SIGQUIT信号是
//dump 线程堆栈还是 attach请求的关键。
File var4 = new File(tmpdir, ".attach_pid" + var3);
createAttachFile(var4.getPath());
try {
//向目标程序发送 SIGQUIT 信号。(此时/tmp//attach_pid+pid文件已经被创建,
//Signal Dispatch 线程(此线程在create_vm被创建启动后一直轮询等待信号产生)
//在收到SIGQUIT信号后,将检测/tmp//attach_pid+pid文件 存在就执行
//attach逻辑即启动Attach Listener 线程完成 socket bind listen准备接收数据,
//不存在则进行线程堆栈dump 操作)
sendQuitTo(var3);
int var5 = 0;
long var6 = 200L;
int var8 = (int)(this.attachTimeout() / var6);//this.attachTimeout()默认值是10000
//等待10秒 期间找到了tmp/.java_pid+pid文件(找到了的话代表Attach Listener线程
//被创建成功可以开始进行attach了) 则往下走,10秒后没找到的话 抛出
//AttachNotSupportedException异常
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);
//基于 tmp/.java_pid+pid 文件创建unix 套接字并连接
int var24 = socket();
try {
//基于此unix 套接字,进行连接 ,之后就可以进程间通信了
//(注意unix套接字,只能同机器不同进程间通信,而不能实现 不同机器间的通信!!!这一点一定要清楚)
connect(var24, this.path);
} finally {
close(var24);
}
}
可以看到最终BsdVirtualMachine
构造方法中的逻辑是
-
查找/tmp目录下是否存在".java_pid"+pid文件(此文件就是unix套接字对应的文件,是unix套接字通信的基础) 如果不存在,则创建tmp/.attach_pid + pid文件(此文件是判断是否是attach的依据,如果找不到这个文件,则进行线程dump了,这个逻辑一会再源码可以看到),这个路径的来源可以参考下边的源码截图:
-
然后发送
SIGQUIT
信号给目标进程,(在sendQuitTo(var3);
这行代码)- 最终调到/Users/hzz/myself_project/jdk_source_code/jdk8/jdk8u/jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c 这个jvm方法里边:
- 信号这个东西展开的话比较复杂我们简单描述下: 信号是某事件发生时对进程的通知机制,也被称为"软件中断"。信号可以看做是一种非常轻量级的进程间通信,信号由一个进程发送给另外一个进程,只不过是经由内核作为一个中间人发出,信号最初的目的是用来指定杀死进程的不同方式。 每个信号都有一个名字,以 "SIG" 开头,最熟知的信号应该是
SIGKILL
因为kill -9 pid
是非常常用的一个命令,而这个9其实就是信号SIGKILL
的编号,kill -9 pid
命令本质就是发送一个SIGKILL
信号给目标进程
,目标进程收到命令后,强制退出。每个信号都有一个唯一的数字标识,从 1 开始。- 下面是常见的标准信号: 因为上边在atach过程中sendQuitTo最终发送的是
SIGQUIT
信号,所以这里我们重点关注这个,像上边标准信号介绍那样,在linux系统中SIGQUIT
信号一般用于退出系统,但是
在 Java 虚拟机(JVM)中,SIGQUIT
信号默认被处理为线程转储
操作也就是说thread dump
。当 JVM 接收到SIGQUIT
信号时,它通常会打印所有 Java 线程的当前堆栈跟踪到标准错误流(stderr)或指定的日志文件,而不是终止进程。这使得SIGQUIT
成为在运行时调试和诊断 Java 应用程序时一个非常有用的工具,如下演示(使用kill -3 pid 发起SIGQUIT信号,目标进程控制台将输出 线程堆栈信息
): 其实jmap jstack这些工具就是通过SIGQUIT来实现的堆栈信息的输出的。but但是
在jvm中 接收到此信号时并不只是用于堆栈信息的输出
(因为我们上边说的是默认,而不是 只是dump线程),还有另一个处理逻辑
就是响应处理attach请求
(这个一会从源码【jdk8u/hotspot/src/share/vm/runtime/os.cpp
的signal_thread_entry
方法中】可以看到)。
- 下面是常见的标准信号: 因为上边在atach过程中sendQuitTo最终发送的是
- 信号这个东西展开的话比较复杂我们简单描述下: 信号是某事件发生时对进程的通知机制,也被称为"软件中断"。信号可以看做是一种非常轻量级的进程间通信,信号由一个进程发送给另外一个进程,只不过是经由内核作为一个中间人发出,信号最初的目的是用来指定杀死进程的不同方式。 每个信号都有一个名字,以 "SIG" 开头,最熟知的信号应该是
- 最终调到/Users/hzz/myself_project/jdk_source_code/jdk8/jdk8u/jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c 这个jvm方法里边:
-
在向目标进程发送
SIGQUIT
信号后,attach发起端会进入一个do while循环
- 如果在10秒后 (目标jvm还没创建Attach Listener线程, Attach Listener线程会创建"/tmp/.java_pid"+pid文件),那么将会抛出异常
AttachNotSupportedException("Unable to open socket file: target process not responding or HotSpot VM not loaded")
) - 此循环的逻辑如下图标红处描述: 如果找到了"/tmp/.java_pid"+pid文件 ,将会进行下边的逻辑
- 如果在10秒后 (目标jvm还没创建Attach Listener线程, Attach Listener线程会创建"/tmp/.java_pid"+pid文件),那么将会抛出异常
-
步骤3找到".java_pid"+pid文件后,attach 发起端将会基于此文件建立unix套接字(socket() )以及进行连接(connect() ),如下:
- Unix 套接字 这个东西比较底层,更加深入的话可以参见书籍《UNIX网络编程卷一》(也称为 UNIX 域套接字,Unix Domain Sockets)是一种在
同一台机器上
的不同进程
之间进行数据交换
的通信机制
。它是一种IPC(进程间通信
)方法,提供了比其他通信方式(如管道和信号)更复杂的通信能力,关于这个知识点 更深入的我们不再展开,总之我们知道通过他 可以实现同一机器上不同进程之间的通信。值得注意的是 他和普通我们说的tcp udp可不一样,不具有不同机器之间的通信能力。另外他的接口和tcp udp非常像 也有 socket,bind ,listen 等方法,等会我们就会看到。我们这里不要和常说的socket(tcp udp这种)机制混淆即可。下图是unix工作机制的简单图解
- Unix 套接字 这个东西比较底层,更加深入的话可以参见书籍《UNIX网络编程卷一》(也称为 UNIX 域套接字,Unix Domain Sockets)是一种在
-
此时正常情况下 目标程序就已经进入监听状态,此时就可以向其发送 数据了,也就是我们代码中的:
loadAgent
attach发起方基本就这些,但是,如果你不结合被attach来看很容易穿不起来,所以紧接着我们看下被attach的目标程序是如何实现的。之后我们总结并画个流程图就清晰了。
被attach的目标程序 源码分析
在 上边的步骤2 我们知道了 ,发起attach的程序会执行sendQutito方法最终会发送一个SIGQUIT信号给被attach的目标进程,那么目标程序是如何执行的呢?首先我们得知道既然发起了SIGQUIT信号,那么目标程序肯定得有监听 然后识别这个信号进行处理吧? 否则没有监听没处理那还怎么玩?而这个监听的线程是在jdk8u/hotspot/src/share/vm/runtime/thread.cpp 的 create_vm
方法中创建的(create_vm我们上边有说过),如下: signal_thread_entry
的代码比较重要,如下: (在 /jdk8u/hotspot/src/share/vm/runtime/os.cpp 中 )
c++
// --------------------- sun.misc.Signal (optional) ---------------------
// SIGBREAK is sent by the keyboard to query the VM state
#ifndef SIGBREAK
#define SIGBREAK SIGQUIT ,对 SIGQUIT 进行 define ,SIGBREAK 就代表 SIGQUIT信号
#endif
// sigexitnum_pd is a platform-specific special signal used for terminating the Signal thread.
static void signal_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
//轮询
while (true) {
int sig;
{
// FIXME : Currently we have not decieded what should be the status
// for this java thread blocked here. Once we decide about
// that we should fix this.
//进入等待 即 当前线程被block
sig = os::signal_wait();
}
if (sig == os::sigexitnum_pd()) {
// Terminate the signal thread
return;
}
//一旦发现有信号,则退出block状态,进行下边处理
switch (sig) {
//如果是SIGQUIT信号 SIGBREAK其实就是SIGQUIT 因为他被defind: #define SIGBREAK SIGQUIT
case SIGBREAK: {
#if INCLUDE_SERVICES
// Check if the signal is a trigger to start the Attach Listener - in that
// case don't print stack traces.
//如果这个信号是用来触发并启动Attach Listener的,则不打印输出堆栈信息。
//DisableAttachMechanism默认是false ,
//此常量定义在了 : jdk8u/hotspot/src/share/vm/runtime/globals.hpp 中
if (!DisableAttachMechanism) {
// Attempt to transit state to AL_INITIALIZING.
jlong cur_state = AttachListener::transit_state(AL_INITIALIZING, AL_NOT_INITIALIZED);
if (cur_state == AL_INITIALIZING) {
// Attach Listener has been started to initialize. Ignore this signal.
//Attach Listener已经启动并初始化 忽略此信号
continue;
} else if (cur_state == AL_NOT_INITIALIZED) {
// Start to initialize.
//开始初始化 执行方法 :AttachListener::is_init_trigger()
if (AttachListener::is_init_trigger()) {
// Attach Listener has been initialized.
// Accept subsequent request.
//Attach Listenerq已经初始化完成,可以开始接收请求了
continue;
} else {
// Attach Listener could not be started.
// So we need to transit the state to AL_NOT_INITIALIZED.
//没启动成功,设置状态为未启动
AttachListener::set_state(AL_NOT_INITIALIZED);
}
} else if (AttachListener::check_socket_file()) {
// Attach Listener has been started, but unix domain socket file
// does not exist. So restart Attach Listener.
//已经启动了Attach Listener ,但是没找到 unix 套接字文件,重启Attach Listener
continue;
}
}
#endif
//如果不是attach请求,则打印堆栈线程信息 也就是JVM对SIGQUIT信号的默认处理行为 比如
//kill -3 pid 这种。
// Print stack traces
// Any SIGBREAK operations added here should make sure to flush
// the output stream (e.g. tty->flush()) after output. See 4803766.
// Each module also prints an extra carriage return after its output.
VM_PrintThreads op;
VMThread::execute(&op);
VM_PrintJNI jni_op;
VMThread::execute(&jni_op);
VM_FindDeadlocks op1(tty);
VMThread::execute(&op1);
Universe::print_heap_at_SIGBREAK();
if (PrintClassHistogram) {
VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */);
VMThread::execute(&op1);
}
if (JvmtiExport::should_post_data_dump()) {
JvmtiExport::post_data_dump();
}
break;
}
default: {
// Dispatch the signal to java 。 非SIGQUI信号 ,即其他信号的处理
HandleMark hm(THREAD);
Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD);
KlassHandle klass (THREAD, k);
if (klass.not_null()) {
JavaValue result(T_VOID);
JavaCallArguments args;
args.push_int(sig);
JavaCalls::call_static(
&result,
klass,
vmSymbols::dispatch_name(),
vmSymbols::int_void_signature(),
&args,
THREAD
);
}
//异常处理 略
}
}
}
}
}
判断是否存在 /tmp/.attach_pid+pid文件!!!!!!!!!从注释即可看出来
// If the file .attach_pid<pid> exists in the working directory
// or /tmp then this is the trigger to start the attach mechanism
bool AttachListener::is_init_trigger() {
if (init_at_startup() || is_initialized()) {
return false; // initialized at startup or already initialized
}
char path[PATH_MAX + 1];
int ret;
struct stat st;
//构建文件名称并判断
snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d",
os::get_temp_directory(), os::current_process_id());
RESTARTABLE(::stat(path, &st), ret);
//ret==0 说明存在
if (ret == 0) {
// simple check to avoid starting the attach mechanism when
// a bogus user creates the file
if (st.st_uid == geteuid()) {
//初始化Attach Listener线程
init();
return true;
}
}
//返回false说明 .attach_pid+pid文件不存在,不进行attach操作
return false;
}
// The Attach Listener threads services a queue. It dequeues an operation
// from the queue, examines the operation name (command), and dispatches
// to the corresponding function to perform the operation.
static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
thread->record_stack_base_and_size();
//AttachListener::pd_init()中会创建 绑定 监听
if (AttachListener::pd_init() != 0) {
AttachListener::set_state(AL_NOT_INITIALIZED);
return;
}
AttachListener::set_initialized();
for (;;) {
//从队列取出 attach请求
AttachOperation* op = AttachListener::dequeue();
if (op == NULL) {
AttachListener::set_state(AL_NOT_INITIALIZED);
return; // dequeue failed or shutdown
}
//进行处理 即 找到对应function进行处理调用。这里的代码略,一会我们截图看下
}
int AttachListener::pd_init() {
JavaThread* thread = JavaThread::current();
ThreadBlockInVM tbivm(thread);
thread->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition() or
// java_suspend_self() via check_and_wait_while_suspended()
//init中会 : 创建,绑定,监听 unix套接字,之后就可以和发起attach方进行通信了
int ret_code = BsdAttachListener::init();
// were we externally suspended while we were waiting?
thread->check_and_wait_while_suspended();
return ret_code;
}
这段就是建立unix 连接,绑定,监听的代码了:
c++
// Initialization - create a listener socket and bind it to a file
int BsdAttachListener::init() {
char path[UNIX_PATH_MAX]; // socket file
char initial_path[UNIX_PATH_MAX]; // socket file during setup
int listener; // listener socket (file descriptor)
// register function to cleanup
if (!_atexit_registered) {
_atexit_registered = true;
::atexit(listener_cleanup);
}
int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
os::get_temp_directory(), os::current_process_id());
if (n < (int)UNIX_PATH_MAX) {
n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
}
if (n >= (int)UNIX_PATH_MAX) {
return -1;
}
// create the listener socket
listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
if (listener == -1) {
return -1;
}
// bind socket
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, initial_path);
::unlink(initial_path);
int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));
if (res == -1) {
::close(listener);
return -1;
}
// put in listen mode, set permissions, and rename into place
res = ::listen(listener, 5);
if (res == 0) {
RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
if (res == 0) {
// make sure the file is owned by the effective user and effective group
// (this is the default on linux, but not on mac os)
RESTARTABLE(::chown(initial_path, geteuid(), getegid()), res);
if (res == 0) {
res = ::rename(initial_path, path);
}
}
}
if (res == -1) {
::close(listener);
::unlink(initial_path);
return -1;
}
set_path(path);
set_listener(listener);
return 0;
}
上边就行被attach的目标程序的实现了,比较重要,总结一下:
- 首先在create_vm阶段调用os::sigle_init,里边创建Signal Dispatcher线程,这个线程中(signal_thread_entry)会执行while(true)循环, 进入轮询状态
- 之后进入阻塞等待(wait)等待接收各种: SIGNAL 信号
- 有信号后,进行下边的处理,
- 如果是SIGQUIT 信号,且存在tmp/.java_pid+"pid"文件 ,则认为是attach请求
- 如果存在 tmp/.attach_pid+"pid"文件(在此方法中判断的:AttachListener::is_init_trigger()) 则认为是attach请求,则启动并初始化Attach Listener 线程,初始化时会进行 unix套接字的创建(socket),绑定(bind),监听(listen)
- 如果是SIGQUIT 信号但是不存在tmp/.attach_pid+"pid"文件 ,则进行打印输出 线程堆栈信息,也即JVM对SIGQUIT的默认处理,参见jstack这类的操作。如下所示:
- 如果是SIGQUIT 信号,且存在tmp/.java_pid+"pid"文件 ,则认为是attach请求
loadAgent ->请求目标程序加载agent jar
具体为发起attach的 这行代码:
java
vm.loadAgent("/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
上边这个代码最终调用到了以下方法,就是向这个unix套接字写入具体的数据,目的就是要求 被attach进程,load 指定的agent jar 具体发送的内容就是 load instrument 你的jar路径 大概就是这个意思,如果想看详情使用strace 或 truss命令。
服务端accept并查找指定key对应的function
funcs定义如下(注意从此方法可以看出:动态加载agent 能做的所有事情就是这些了):
c++
// Table to map operation names to functions.
// names must be of length <= AttachOperation::name_length_max
static AttachOperationFunctionInfo funcs[] = {
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
{ "dumpheap", dump_heap },
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ NULL, NULL }
};
因为我们在vm.laodAgnet(jarPath)时 laodAgnet内部最终传入的是 load 命令,也就是说会找JvmtiExport::load_agent_library这个函数,如下是JvmtiExport::load_agent_library对应的逻辑 另外因为laodAgnet中传入的链接库名称是instrument,(可以从laodAgnet源码看这里字数限制就不贴了)所以最终找到动态链接库就是libinstrument.so然后去执行 libinstrument.so的Agent_OnLoad方法,看到这里我想你应该明白后续的流程了,后边就和静态加载差不多了,如下:
动态加载 Java Agent图解(重要重要重要)
如下图所示:
5、一些自言自语
这篇文章酝酿至少好几个月其中被各种事情打断,另外也写了很久(断断续续一个月差不多)也是我有史以来最长的一篇,整个下来收获蛮多的,虽然java agent在实际直接开发中用的不多,但是并不代表他不重要,严格来讲我们每天都在使用依赖java agent开发的各种软件或框架。
有时候在想
- 是什么支持我一点点扣这些底层源码的?
- 是什么在我眼睛酸涩的情况下任然想打开电脑去深究?
- 是什么驱使我 吃饭/地铁/睡前 都在想某个实现细节的?
是为了提升技术,是为了装13?是骨子里的执念?是对茅塞顿开的感觉上了瘾?我想这些都不重要了。 重要的是:我开心就好,仅此而已。
本文参考了 "你假笨" 大佬的文章和2篇官网文章:
lovestblog.cn/blog/2015/0... docs.oracle.com/javase/8/do... www.ibm.com/docs/en/sdk...
如果已经看到了这里的请帮忙😂,点个赞👍🏻👍🏻👍🏻。如果没有人看,那我就将此篇文章献给此刻迷茫的自己:孤芳独自赏,深情共白头。
2024.04.25