说明:本文很长,长到超出了掘金编辑器的限制字符数10万,所以我在最后边只是图解,没有更多的文字和代码描述了,本文知识点较多,如果没接触过agent那必然大概率会懵(大部分知识点讲解完后,我都会配个图来总结归纳加强理解)。当你一点点去理解尝试后相信会有所收获,另外水平有限不对地方请指导
本文大概结构:
本文涉及到的知识点:
在开始之前,我们先来了解几个重要的内容,先对这些东西有个大体概念。
接下来,我们深入展开讲解下以上这些知识点。
JavaAgent是Java平台提供的一种特殊机制,它允许开发者在Java应用程序(被jvm加载/正在被jvm运行)时注入我们指定的字节码。这种技术被广泛应用于功能增强、监控、性能分析、调试、信息收集等多种场景,JavaAgent依赖于instrument这个特殊的JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(JavaProgrammingLanguageInstrumentationServicesAgent),专门为Java语言编写的插桩服务提供支持的,JavaAgent有两种加载时机,分别是:
注意:(这里只简单文字描述,详细内容和源码放到后边讲解)
动态加载JavaAgent主要依赖于JavaInstrumentationAPI的agentmain方法和AttachAPI。具体步骤如下:
上边我们也提到过JVMTI,而如果你学习了解agent那么深入理解JVMTI将是必不可少要学习的。下边就来详细说下
JVMTI全称:(JavaVirtualMachineToolInterface),简单来说就是jvm暴露出来的一些供用户扩展的回调接口集合,有一点我们要知道,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件对应的回调接口。而通过这个回调机制,我们实际上就可以实现与JVM的“互动”。可不要小看这个回调机制,他是n多个框架的底层依赖,没有这个JVMTI回调机制,这些框架也许不能诞生或者需要使用其他更复杂的技术。既然回调机制如此重要,那么都有哪些回调呢?让我们从源码中获取这个内容,如下:
以下是hotspot的JVMTI中定义的一系列回调函数,(暂时我们定义这段代码片段为code1,以便后边引用):
VMInit:当虚拟机初始化时触发,在此时会注册类加载时的回调函数和调用的premain方法(在源码小节会说到)。
ClassFileLoadHook:类加载时调用此钩子函数的实现ClassFileTransformer的transform
ThreadStart:线程启动时触发。
Exception:方法执行过程中抛出异常时触发。
MonitorContendedEnter:线程尝试进入已被其他线程占用的监视器时触发。
FieldAccess:访问字段时触发。
GarbageCollectionStart:垃圾收集开始时触发。
这些事件回调为Java应用和工具提供了深入虚拟机内部操作的能力,从而能够进行更加精细的监控和调试。开发者可以根据需要注册监听特定的事件,本质上也就是我们说的开发者与JVM的”互动“。
接下来我们看下JVMTI的主要功能,其实如果你看了上边的回调节点,基本上可以猜到他主要能干些啥,因为这些功能都是靠实现上边这些回调节点来开发的。
功能:
场景:
文字描述你可能感觉不到什么,但是如果提到这些框架,你大概率会知晓其中的一个或者几个,而他们就是基于JavaAgent实现,而JavaAgent本质上是需要依赖JVMTI的,所以可以说这些大名鼎鼎的框架直接/间接上都是依赖了JVMTI,比如下边这些:
热加载类:
链路追踪类
开发调试类:
当然,肯定还有很多我不知道的框架亦或者插件直接或者间接使用到了JVMTI,这里我们不过多讨论了。上边简单介绍了JVMTI是什么,以及他的功能和使用场景,以及一些直接/间接使用到他的框架。下边我们就看看如何直接实现JVMTIAgent。
我们下边就给他使用c代码实现一个JVMTI中ClassFileLoadHook,这个钩子函数中的逻辑比较简单,它演示了如何使用c语言设置ClassFileLoadHook事件回调,并在回调函数中简单地打印被加载的类的名称(注意:此处小案例使用了启动时静态加载,如果要动态加载需要实现Agent_OnAttach函数,这里我们不做演示)。步骤如下:
1.创建JVMTIAgent:
创建一个名为ClassFileLoadHookAgent.c的C文件,用于实现JVMTIAgent:
gcc-shared-fPIC-I${JAVA_HOME}/include-I${JAVA_HOME}/include/linux-o>classfileloadhookagent.soClassFileLoadHookAgent.c这里${JAVA_HOME}是你JDK的安装目录,这条命令会生成一个名为classfileloadhookagent.so的共享库(动态链接库linux中一般以.so结尾之前说过了)文件。
3.运行Agent:使用-agentpath参数将你的Agent附加到Java应用程序。并使用java命令执行编译后的class文件,如下:
下面进行演示,如下:
(注意代码中是去掉包名的因为这样我们只需要javaNativeCodeImplClassFileLoadHookTest就可以执行class文件了,有包名的话还得全限定所以我们就不加包名了)
上边我们讲解了JavaAgent和JVMTI以及如何实现一个JVMTIAgent,到这里相信你已经有所了解,接下来我们就编写几个agent案例并分别分析他们的实现原理以及源码流程。让我们对agent的工作机制以及底层实现有更深入的认识。
ps:静态加载和动态加载区别还是比较大的,所以我打算把他们分开各说各的,以免混淆。
(一些比较细的东西,我都放到代码注释中了,在代码外就不额外啰嗦了)
使用命令解压jar:
编写并执行main方法,这里我们很重要的一步就是在vm参数中配置了此内容:
-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar)从而让jvm启动时(也即静态)加载我们编写的agentjar,使得在执行main方法里的getTime方法时执行的是我们修改替换(transform)后的,修改后的getTime方法体内容是:
以上就是静态加载的demo了,虽然很简单,但是麻雀虽小五脏俱全了也算是,趁热打铁吧,下边我们就从源码角度来逐步分析静态加载实现的流程与原理,注意源码小节比较重要,看完源码,才会有恍然大悟的感觉。没错我就是这个感觉。
完整代码在:/hotspot/src/share/vm/runtime/arguments.cpp中
此片段的完整源码在/hotspot/src/share/vm/runtime/thread.cpp中
紧接着我们大概看下是怎么找的
在上边的create_vm_init_agents函数中我们查找并执行了动态链接库libinstrument.so中的Agnet_OnLoad函数,而这个函数最终会执行到InvocationAdapter.c的Agent_OnLoad中,下边是此方法的代码:
这个方法的注释很重要(见下边代码中的注释),这里简单翻译下
我们来看下createNewJPLISAgent的代码:
源码在:jdk8u/jdk/src/share/instrument/JPLISAgent.c
接下来就是通过post_vm_initialized来执行(在initializeJPLISAgent中)提前设置好的vm初始化回调事件即:eventHandlerVMInit。
eventHandlerVMInit方法比较重要,紧接着我们来看下:
源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c
源码在:/jdk8u/jdk/src/share/instrument/JPLISAgent.c
但是有一点我们要清楚,这里只是设置回调函数,并没有真正执行eventHandlerClassFileLoadHook的内容,因为此时还不到类加载阶段,切记这一点
在这个eventHandlerClassFileLoadHook里边会最终调用(注意不是此时调用,而是类加载时)到我们的jdk中的ClassFileTransformer接口的transform方法,接下来我们看下:
voidJNICALLeventHandlerClassFileLoadHook(jvmtiEnv*jvmtienv,JNIEnv*jnienv,jclassclass_being_redefined,jobjectloader,constchar*name,jobjectprotectionDomain,jintclass_data_len,constunsignedchar*class_data,jint*new_class_data_len,unsignedchar**new_class_data){JPLISEnvironment*environment=NULL;environment=getJPLISEnvironment(jvmtienv);/*ifsomethingisinternallyinconsistent(noagent),justsilentlyreturnwithouttouchingthebuffer*/if(environment!=NULL){jthrowableoutstandingException=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代码,见下边:
在开启监听类加载事件并注册完类加载时的回调函数后,进行下边逻辑
注意:loadClassAndCallPremain中会调用loadClassAndStartAgent方法
java代码如下:
那么什么时候会执行(或者说回调,这个词更符合此函数的调用动作)到我的DefineTransformer类中的tranform方法去修改(Retransform)或者重新定义(Redefine)类呢?那肯定是类加载时啊,上边我们说过很多遍了!
注意我们本文中的DefineTransformer类实现了java.lang.instrument.ClassFileTransformer接口的transform方法!所以才会调用到DefineTransformer类的transform方法!这一点要明白!
继续跟进load_calassfile中的parseClassFile方法:ps:这个方法巨长,至少有600多行,类加载的主要逻辑就在这里边了,感兴趣可以去看看完整的,这里我们不粘完整版本了,只保留我们感兴趣的,调用类加载时候的钩子函数片段,代码如下:
在初始化这一步之后,类的元数据被保存到了元空间(1.8后引入的)中,之后我们就可以愉快的使用了,比如new或者反射等等根据类元数据创建实例这类行为,或者访问类的元数据比如类.class等等操作。
到此,就算真正的将静态加载jar以及插桩是如何执行的这些流程串联起来了。真不容易。我都不知道我怎么坚持下来的整个流程比较复杂,观看代码太枯燥,还是画个图吧,更直观(一图胜千言!)如下:
到此,你清楚静态加载时JavaAgent的工作原理和机制了吗???ok接下来我们说说动态加载。
动态加载相较于静态加载,会更灵活一点,我们演示下如何实现一个动态加载的agent。
修改前的Integer.value(inti);代码:
publicstaticIntegervalueOf(inti){returnnewInteger(i);}修改jdk代码肯定是行不通,人家也不让你直接修改,我们这里准备通过agent修改,然后动态加载agentjar,是不是很熟悉?没错热部署就是类似的原理。即不用重启即让代码生效。
基本上编写一个agentjar需要三个内容
截图放不下,直接贴代码:
上边简单演示了下动态加载的使用,下边我们还是称热打铁从源码角度分析
com.sun.tools.attach.VirtualMachine类的attach方法:
类sun.tools.attach.BsdAttachProvider的attachVirtualMachine方法如下:
publicVirtualMachineattachVirtualMachine(Stringvar1)throwsAttachNotSupportedException,IOException{this.checkAttachPermission();this.testAttachable(var1);returnnewBsdVirtualMachine(this,var1);}类:sun.tools.attach.BsdVirtualMachine的构造方法:
attach发起方基本就这些,但是,如果你不结合被attach来看很容易穿不起来,所以紧接着我们看下被attach的目标程序是如何实现的。之后我们总结并画个流程图就清晰了。
具体为发起attach的这行代码:
有时候在想
是为了提升技术,是为了装13?是骨子里的执念?是对茅塞顿开的感觉上了瘾?我想这些都不重要了。重要的是:我开心就好,仅此而已。