关于Java Agent的使用、工作原理、及hotspot源码解析
关于Java Agent的使用、工作原理、及hotspot源码解析
本文涉及到的知识点
- JVMTI(Java Virtual Machine Tool Interface)
- JVMTIAgent
- Java Agent
- Java类加载机制
- unix套接字
- 信号机制(signal)
- hotspot源码
- 动态链接库文件(linux中是.so,win中是.dll结尾)
- JNI(java native interface)
- 字节码修改(本文使用的是javassist)
- 钩子(hook)机制:在编译中这个非常重要,不管是底层(如linux)还是上层框架(如spring),此机制都会给软件带来很大的扩展空间和灵活性,是编程中非常常见的一种技术,在下文中回调函数其实就是指的狗子函数,钩子是机制,回调是动作,本文中你叫他钩子函数或者回调函数都是一个意思
前置说明
在开始之前,我们先来了解几个重要内容
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#architecture
- JVMTI (全称:Java Virtual Machine Tool Interface)是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI是基于事件驱动的,JVM 每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户自行可以扩展,JVMTI源码在
jdk8/jdk8u/jdk/src/share/javavm/export/jvmti.h
这个文件中
通过JVMTI可以用来实现profiling性能分析、debugging、监控、线程分析、覆盖率分析等工具。 接口提供的功能分为几大类,包括了class、线程、Heap内存的查询、操作等等。 这样可以在不改动代码的情况下监控、分析java进程的状态等。 javaagent也常用来实现类似的功能,不过javaagent对应的Instrumentation接口的功能相对有限,可以通过JVMTI获取更多底层功能。
- 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语言编写的插桩服务提供支持的。
otool -tV libinstrument.dylib > temp.txt
当我们静态加载agent jar(启动时添加vm参数 -javaagent:xxxjar包路径的方式)时Agent_OnLoad
会调用到我们的premain
方法,当我们动态加载(JVM的attach机制,通过发送load命令来加载)时Agent_OnAttach
会调用到我们的agentmian
方法。
- Instrumentation API 为Java Agent提供了一套
Java层面的接口
,它是在Java 5开始引入的,旨在为Java开发者提供一种标准方式来动态修改类的行为以及做增强操作
- JVMTIAgent 是一个动态链接库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
以上几个知识点之间的关系图如下
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 加载方式
静态加载
即JVM启动时加载,在JVM启动时通过命令行参数-javaagent:path/to/youragent.jar
指定Agent的 jar包。这要求 Agent 的入口类(即agent.jar包中的 META-INF->MAINIFEST.MF文件中的Permain-Class
对应的类)实现 permain
方法,该方法会在应用程序的 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 回调机制,这些框架也许不能诞生或者需要使用其他更复杂的技术。
/* Event Callback Structure */ typedef struct { /* 50 : VM Initialization Event */ jvmtiEventVMInit VMInit; /* 51 : VM Death Event */ jvmtiEventVMDeath VMDeath; /* 52 : Thread Start */ jvmtiEventThreadStart ThreadStart; /* 53 : Thread End */ jvmtiEventThreadEnd ThreadEnd; /* 54 : Class File Load Hook */ jvmtiEventClassFileLoadHook ClassFileLoadHook; /* 55 : Class Load */ jvmtiEventClassLoad ClassLoad; /* 56 : Class Prepare */ jvmtiEventClassPrepare ClassPrepare; /* 57 : VM Start Event */ jvmtiEventVMStart VMStart; /* 58 : Exception */ jvmtiEventException Exception; /* 59 : Exception Catch */ jvmtiEventExceptionCatch ExceptionCatch; /* 60 : Single Step */ jvmtiEventSingleStep SingleStep; /* 61 : Frame Pop */ jvmtiEventFramePop FramePop; /* 62 : Breakpoint */ jvmtiEventBreakpoint Breakpoint; /* 63 : Field Access */ jvmtiEventFieldAccess FieldAccess; /* 64 : Field Modification */ jvmtiEventFieldModification FieldModification; /* 65 : Method Entry */ jvmtiEventMethodEntry MethodEntry; /* 66 : Method Exit */ jvmtiEventMethodExit MethodExit; /* 67 : Native Method Bind */ jvmtiEventNativeMethodBind NativeMethodBind; /* 68 : Compiled Method Load */ jvmtiEventCompiledMethodLoad CompiledMethodLoad; /* 69 : Compiled Method Unload */ jvmtiEventCompiledMethodUnload CompiledMethodUnload; /* 70 : Dynamic Code Generated */ jvmtiEventDynamicCodeGenerated DynamicCodeGenerated; /* 71 : Data Dump Request */ jvmtiEventDataDumpRequest DataDumpRequest; /* 72 */ jvmtiEventReserved reserved72; /* 73 : Monitor Wait */ jvmtiEventMonitorWait MonitorWait; /* 74 : Monitor Waited */ jvmtiEventMonitorWaited MonitorWaited; /* 75 : Monitor Contended Enter */ jvmtiEventMonitorContendedEnter MonitorContendedEnter; /* 76 : Monitor Contended Entered */ jvmtiEventMonitorContendedEntered MonitorContendedEntered; /* 77 */ jvmtiEventReserved reserved77; /* 78 */ jvmtiEventReserved reserved78; /* 79 */ jvmtiEventReserved reserved79; /* 80 : Resource Exhausted */ jvmtiEventResourceExhausted ResourceExhausted; /* 81 : Garbage Collection Start */ jvmtiEventGarbageCollectionStart GarbageCollectionStart; /* 82 : Garbage Collection Finish */ jvmtiEventGarbageCollectionFinish GarbageCollectionFinish; /* 83 : Object Free */ jvmtiEventObjectFree ObjectFree; /* 84 : VM Object Allocation */ jvmtiEventVMObjectAlloc VMObjectAlloc; } jvmtiEventCallbacks;
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 允许工具通过事件获取 JVM 内发生的特定情况的通知,如线程启动/结束、类加载/卸载、方法进入/退出等。
- 线程管理:它提供了监控和管理 Java 程序中线程状态的能力。
- 堆和垃圾回收:JVMTI 支持查询堆信息、监控垃圾回收事件,以及在某些条件下控制垃圾回收的执行。
- 调试支持:JVMTI 为调试器提供了丰富的接口,支持断点、单步执行、字段访问/修改等调试功能。
- 性能监测:提供了监视和分析 JVM 性能的工具,如获取方法执行时间、内存使用情况等。
场景
- 开发调试工具:利用 JVMTI 提供的调试支持,开发强大的调试工具,比如 idea, eclipse等等。
- 性能分析:构建性能分析工具来识别 Java 应用的性能瓶颈。
- 监控工具:创建监控工具来实时监视 JVM 的健康状况和性能指标。
- 覆盖率分析:通过跟踪类和方法的加载与执行,帮助生成代码覆盖率报告。
运行时监控&性能分析类:
- VisualVM:是JDK自带的一个用于Java程序性能分析的可视化工具,通过他可以获取应用程序的,堆,内存,线程,cpu,快照等等运行时信息。
- JProfiler:和VisualVM类似,也是能获取Java应用程序以及jvm的各种信息。
- BTrace:是一个监控&追踪工具,可以监控程序状态,获取运行时数据信息,如方法返回值,参数,调用次数,全局变量,调用堆栈等。
- Arthas: 是阿里的一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
- Greys:是一个JVM进程执行过程中的异常诊断工具,可以在不中断程序执行的情况下轻松完成问题排查工作。其实他也是模仿了BTrace
热加载类:
- HotSwapAgent:是一个免费的开源插件,它扩展了JVM内置的HotSwap机制的功能
- 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 应用运行状态的数据,例如方法调用、变量状态、线程信息等。注意: idea debug 其实不单单仅靠一个agent实现,他的实现是基于Java Platform Debugger Architecture (JPDA),即Java 平台调试架构,这个架构包含3部分 (JVMTI(JVM Tool Interface)、JDWP(Java Debug Wire Protocol)、JDI(Java Debug Interface))
使用 C 编写一个 JVMTIAgent
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代码实现
我们下边就给他使用c代码实现一个 JVMTI中 ClassFileLoadHook, 这个钩子函数中的逻辑比较简单,它演示了如何使用c语言设置ClassFileLoadHook
事件回调,并在回调函数中简单地打印被加载的类的名称(注意: 此处小案例使用了启动时静态加载
。)
- 创建 JVMTI Agent
// 这些是必需的头文件。jvmti.h提供了与Java虚拟机工具接口相关的声明,而stdio.h和stdlib.h则用于标准的输入输出和内存管理函数。 #include <jvmti.h> #include <stdio.h> #include <stdlib.h> // ClassFileLoadHook回调函数 // 在类文件加载时被调用 void JNICALL ClassFileLoadHook( jvmtiEnv *jvmti_env, // JVMTI环境指针 JNIEnv* jni_env, // JNI环境指针 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; }
- 编译 Agent编译这个Agent 需要依赖 操作系统和 JDK安装路径。
gcc -shared -fPIC -I/Users/nowcoder/jdk8/Contents/Home/include -I/Users/nowcoder/jdk8/Contents/Home/include/darwin -o classFileLoadHookAgent.dylib ClassFileLoadHookAgent.c
这个命令会生成一个名为classFileLoadHookAgent.dylib的共享库(动态链接库 linux中一般以 .so 结尾)
- 运行 Agent
使用-agentpath
参数将你的Agent附加到Java应用程序。并使用java命令执行编译后的class文件
javac MainTest.java java -agentpath://Users/nowcoder/code/my-home/example-c/classFileLoadHookAgent.dylib MainTest
可以看到通过在 ClassFileLoadHookAgent.c中实现函数 Agent_OnLoad
并设置&开启回调事件ClassFileLoadHook,成功的让jvm在加载类时调用了回调函数,也就是执行了这段代码: `printf("使用c编写ClassFileLoadHook的实现_当前加载的类名称是: %s\n", name);
3. Java Agent 静态加载演示、图解、源码分析
静态加载demo实现与演示
想要达成的效果
通过agent插桩的方式修改Date类的getTime()方法,使其返回的时间戳为:秒级别而不是毫秒级
通过Instrument API 和 javaassist 编写插桩代码
https://www.javassist.org/tutorial/tutorial.html
package com.example.javaagent.demos.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 lijingyang * @date 2024/07/04 */ 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 currentTimeMillis = getTimeImpl();" + "System.put.println( 使用agent 探针对Date 方法进行修改并打印,当前时间【毫秒级】: + currentTimeMillis);" + "return currentTimeMillis / 1000L;" + "}"; getTime.setBody(methodBody); // 通过 CtClass 的 toBytecode(); 方法来获取 被修改后的字节码 return clazz.toBytecode(); } catch (Exception e) { e.printStackTrace(); } finally { if (clazz != null) { // 调用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 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,permain方法在哪个类中 --> <Premain-Class>com.example.javaagent.demos.config.JdkDateAgentTest</Premain-Class> <!-- 该属性设置为 true 时表示:允许已被加载的类被重新转换(retransform)。这意味着 Java Agent 可以在运行时修改已经加载的类的字节码,而不需要冲洗启动应用或 JVM。 --> <!-- 注意,如果此属性设置为 false 在执行main方法且设置 -javaagent.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 在运行时重新定义(也就是完全替换)已加载的类的字节码。 --> <Can-Redefine-Classes>false</Can-Redefine-Classes> <!-- 该属性设置为 true 时表示:允许 Java Agent 在运行时动态地为 JNI (Java Native Interface)方法设置前缀。 --> <!-- 这项能力主要用于修改或拦截本地方法的调用。 --> <Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix> <!-- 指定 agentmain 方法的入口类(动态加载时将会调用 agentmain 方法)--> <!-- <Agent-Class></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>
打包
解压 jar 并查看 /META-INF/MANIFEST.MF 文件内容
jar -xvf JavaAgent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
编写&执行main方法(使用 -javaagent静态加载上边的 agent jar 包)
这里我们很重要的一步就是在 vm参数中配置了 此内容:
-javaagent:/Users/nowcoder/code/my-home/JavaAgent-0.0.1-SNAPSHOT-jar-with-dependencies/JavaAgent-0.0.1-SNAPSHOT-jar-with-dependencies.jar
也就是我们所说的: 静态加载。
我们通过在main方法启动时添加vm参数,从而让jvm启动时(也即静态)加载我们编写的agent jar,使得在执行main方法里的getTime方法时执行的是我们修改替换(transform)后的
{ long currentTimeMillis = getTimeImpl(); System.out.println("使用agent 探针对Date 方法进行修改并打印,当前时间【毫秒级】: " + currentTimeMillis ); return currentTimeMillis / 1000L; }
这就是所谓的 插桩。
静态加载源码解析
解析启动时传入的vm参数
静态加载 agent 时我们必须使用 -javaagent:xxx.jar ,那么 jvm是如何识别传入的参数并解析运作呢
接下来到 parse_each_vm_init_arg 这个里边,因为vm的参数很多,所以这个函数的代码也特别长。这里我们只关心 -javaagent,其他的略微了解即可。
jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args, SysClassPath* scp_p, bool* scp_assembly_required_p, Flag::Flags origin) { // Remaining part of option string const char* tail; // iterate over arguments for (int index = 0; index < args->nOptions; index++) { bool is_absolute_path = false; // for -agentpath vs -agentlib const JavaVMOption* option = args->options + index; if (!match_option(option, "-Djava.class.path", &tail) && !match_option(option, "-Dsun.java.command", &tail) && !match_option(option, "-Dsun.java.launcher", &tail)) { // add all jvm options to the jvm_args string. This string // is used later to set the java.vm.args PerfData string constant. // the -Djava.class.path and the -Dsun.java.command options are // omitted from jvm_args string as each have their own PerfData // string constant object. build_jvm_args(option->optionString); } // -verbose:[class/gc/jni] if (match_option(option, "-verbose", &tail)) { if (!strcmp(tail, ":class") || !strcmp(tail, "")) { FLAG_SET_CMDLINE(bool, TraceClassLoading, true); FLAG_SET_CMDLINE(bool, TraceClassUnloading, true); } else if (!strcmp(tail, ":gc")) { FLAG_SET_CMDLINE(bool, PrintGC, true); } else if (!strcmp(tail, ":jni")) { FLAG_SET_CMDLINE(bool, PrintJNIResolving, true); } // -da / -ea / -disableassertions / -enableassertions // These accept an optional class/package name separated by a colon, e.g., // -da:java.lang.Thread. } else if (match_option(option, user_assertion_options, &tail, true)) { bool enable = option->optionString[1] == 'e'; // char after '-' is 'e' if (*tail == '\0') { JavaAssertions::setUserClassDefault(enable); } else { assert(*tail == ':', "bogus match by match_option()"); JavaAssertions::addOption(tail + 1, enable); } // -dsa / -esa / -disablesystemassertions / -enablesystemassertions } else if (match_option(option, system_assertion_options, &tail, false)) { bool enable = option->optionString[1] == 'e'; // char after '-' is 'e' JavaAssertions::setSystemClassDefault(enable); // -bootclasspath: } else if (match_option(option, "-Xbootclasspath:", &tail)) { scp_p->reset_path(tail); *scp_assembly_required_p = true; // -bootclasspath/a: } else if (match_option(option, "-Xbootclasspath/a:", &tail)) { scp_p->add_suffix(tail); *scp_assembly_required_p = true; // -bootclasspath/p: } else if (match_option(option, "-Xbootclasspath/p:", &tail)) { scp_p->add_prefix(tail); *scp_assembly_required_p = true; // -Xrun } else if (match_option(option, "-Xrun", &tail)) { if (tail != NULL) { const char* pos = strchr(tail, ':'); size_t len = (pos == NULL) ? strlen(tail) : pos - tail; char* name = (char*)memcpy(NEW_C_HEAP_ARRAY(char, len + 1, mtInternal), tail, len); name[len] = '\0'; char *options = NULL; if(pos != NULL) { size_t len2 = strlen(pos+1) + 1; // options start after ':'. Final zero must be copied. options = (char*)memcpy(NEW_C_HEAP_ARRAY(char, len2, mtInternal), pos+1, len2); } #if !INCLUDE_JVMTI if ((strcmp(name, "hprof") == 0) || (strcmp(name, "jdwp") == 0)) { jio_fprintf(defaultStream::error_stream(), "Profiling and debugging agents are not supported in this VM\n"); return JNI_ERR; } #endif // !INCLUDE_JVMTI add_init_library(name, options); } // -agentlib and -agentpath //与agent相关的,可以看到 不管是 -agentlib 还是-agentpath还是-javaagent, //最终都会执行到一个函数即:add_init_agent } 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 if (valid_hprof_or_jdwp_agent(name, is_absolute_path)) { jio_fprintf(defaultStream::error_stream(), "Profiling and debugging agents are not supported in this VM\n"); return JNI_ERR; } #endif // !INCLUDE_JVMTI add_init_agent(name, options, is_absolute_path); } // -javaagent } else if (match_option(option, "-javaagent:", &tail)) { #if !INCLUDE_JVMTI jio_fprintf(defaultStream::error_stream(), "Instrumentation agents are not supported in this VM\n"); return JNI_ERR; #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); add_init_agent("instrument", options, false); } #endif // !INCLUDE_JVMTI // -Xnoclassgc } else if (match_option(option, "-Xnoclassgc", &tail)) { FLAG_SET_CMDLINE(bool, ClassUnloading, false); // -Xincgc: i-CMS } else if (match_option(option, "-Xincgc", &tail)) { FLAG_SET_CMDLINE(bool, UseConcMarkSweepGC, true); FLAG_SET_CMDLINE(bool, CMSIncrementalMode, true); // -Xnoincgc: no i-CMS } else if (match_option(option, "-Xnoincgc", &tail)) { FLAG_SET_CMDLINE(bool, UseConcMarkSweepGC, false); FLAG_SET_CMDLINE(bool, CMSIncrementalMode, false); // -Xconcgc } else if (match_option(option, "-Xconcgc", &tail)) { FLAG_SET_CMDLINE(bool, UseConcMarkSweepGC, true); // -Xnoconcgc } else if (match_option(option, "-Xnoconcgc", &tail)) { FLAG_SET_CMDLINE(bool, UseConcMarkSweepGC, false); // -Xbatch } else if (match_option(option, "-Xbatch", &tail)) { FLAG_SET_CMDLINE(bool, BackgroundCompilation, false); // -Xmn for compatibility with other JVM vendors } else if (match_option(option, "-Xmn", &tail)) { julong long_initial_young_size = 0; ArgsRange errcode = parse_memory_size(tail, &long_initial_young_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid initial young generation size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, MaxNewSize, (uintx)long_initial_young_size); FLAG_SET_CMDLINE(uintx, NewSize, (uintx)long_initial_young_size); // -Xms } else if (match_option(option, "-Xms", &tail)) { julong long_initial_heap_size = 0; // an initial heap size of 0 means automatically determine ArgsRange errcode = parse_memory_size(tail, &long_initial_heap_size, 0); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid initial heap size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } set_min_heap_size((uintx)long_initial_heap_size); // Currently the minimum size and the initial heap sizes are the same. // Can be overridden with -XX:InitialHeapSize. FLAG_SET_CMDLINE(uintx, InitialHeapSize, (uintx)long_initial_heap_size); // -Xmx } else if (match_option(option, "-Xmx", &tail) || match_option(option, "-XX:MaxHeapSize=", &tail)) { julong long_max_heap_size = 0; ArgsRange errcode = parse_memory_size(tail, &long_max_heap_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid maximum heap size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, MaxHeapSize, (uintx)long_max_heap_size); // Xmaxf } else if (match_option(option, "-Xmaxf", &tail)) { char* err; int maxf = (int)(strtod(tail, &err) * 100); if (*err != '\0' || *tail == '\0' || maxf < 0 || maxf > 100) { jio_fprintf(defaultStream::error_stream(), "Bad max heap free percentage size: %s\n", option->optionString); return JNI_EINVAL; } else { FLAG_SET_CMDLINE(uintx, MaxHeapFreeRatio, maxf); } // Xminf } else if (match_option(option, "-Xminf", &tail)) { char* err; int minf = (int)(strtod(tail, &err) * 100); if (*err != '\0' || *tail == '\0' || minf < 0 || minf > 100) { jio_fprintf(defaultStream::error_stream(), "Bad min heap free percentage size: %s\n", option->optionString); return JNI_EINVAL; } else { FLAG_SET_CMDLINE(uintx, MinHeapFreeRatio, minf); } // -Xss } else if (match_option(option, "-Xss", &tail)) { julong long_ThreadStackSize = 0; ArgsRange errcode = parse_memory_size(tail, &long_ThreadStackSize, 1000); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid thread stack size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } // Internally track ThreadStackSize in units of 1024 bytes. FLAG_SET_CMDLINE(intx, ThreadStackSize, round_to((int)long_ThreadStackSize, K) / K); // -Xoss } else if (match_option(option, "-Xoss", &tail)) { // HotSpot does not have separate native and Java stacks, ignore silently for compatibility } else if (match_option(option, "-XX:CodeCacheExpansionSize=", &tail)) { julong long_CodeCacheExpansionSize = 0; ArgsRange errcode = parse_memory_size(tail, &long_CodeCacheExpansionSize, os::vm_page_size()); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid argument: %s. Must be at least %luK.\n", option->optionString, os::vm_page_size()/K); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, CodeCacheExpansionSize, (uintx)long_CodeCacheExpansionSize); } else if (match_option(option, "-Xmaxjitcodesize", &tail) || match_option(option, "-XX:ReservedCodeCacheSize=", &tail)) { julong long_ReservedCodeCacheSize = 0; ArgsRange errcode = parse_memory_size(tail, &long_ReservedCodeCacheSize, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid maximum code cache size: %s.\n", option->optionString); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, ReservedCodeCacheSize, (uintx)long_ReservedCodeCacheSize); //-XX:IncreaseFirstTierCompileThresholdAt= } else if (match_option(option, "-XX:IncreaseFirstTierCompileThresholdAt=", &tail)) { uintx uint_IncreaseFirstTierCompileThresholdAt = 0; if (!parse_uintx(tail, &uint_IncreaseFirstTierCompileThresholdAt, 0) || uint_IncreaseFirstTierCompileThresholdAt > 99) { jio_fprintf(defaultStream::error_stream(), "Invalid value for IncreaseFirstTierCompileThresholdAt: %s. Should be between 0 and 99.\n", option->optionString); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, IncreaseFirstTierCompileThresholdAt, (uintx)uint_IncreaseFirstTierCompileThresholdAt); // -green } else if (match_option(option, "-green", &tail)) { jio_fprintf(defaultStream::error_stream(), "Green threads support not available\n"); return JNI_EINVAL; // -native } else if (match_option(option, "-native", &tail)) { // HotSpot always uses native threads, ignore silently for compatibility // -Xsqnopause } else if (match_option(option, "-Xsqnopause", &tail)) { // EVM option, ignore silently for compatibility // -Xrs } else if (match_option(option, "-Xrs", &tail)) { // Classic/EVM option, new functionality FLAG_SET_CMDLINE(bool, ReduceSignalUsage, true); } else if (match_option(option, "-Xusealtsigs", &tail)) { // change default internal VM signals used - lower case for back compat FLAG_SET_CMDLINE(bool, UseAltSigs, true); // -Xoptimize } else if (match_option(option, "-Xoptimize", &tail)) { // EVM option, ignore silently for compatibility // -Xprof } else if (match_option(option, "-Xprof", &tail)) { #if INCLUDE_FPROF _has_profile = true; #else // INCLUDE_FPROF jio_fprintf(defaultStream::error_stream(), "Flat profiling is not supported in this VM.\n"); return JNI_ERR; #endif // INCLUDE_FPROF // -Xconcurrentio } else if (match_option(option, "-Xconcurrentio", &tail)) { FLAG_SET_CMDLINE(bool, UseLWPSynchronization, true); FLAG_SET_CMDLINE(bool, BackgroundCompilation, false); FLAG_SET_CMDLINE(intx, DeferThrSuspendLoopCount, 1); FLAG_SET_CMDLINE(bool, UseTLAB, false); FLAG_SET_CMDLINE(uintx, NewSizeThreadIncrease, 16 * K); // 20Kb per thread added to new generation // -Xinternalversion } else if (match_option(option, "-Xinternalversion", &tail)) { jio_fprintf(defaultStream::output_stream(), "%s\n", VM_Version::internal_vm_info_string()); vm_exit(0); #ifndef PRODUCT // -Xprintflags } else if (match_option(option, "-Xprintflags", &tail)) { CommandLineFlags::printFlags(tty, false); vm_exit(0); #endif // -D } else if (match_option(option, "-D", &tail)) { if (CheckEndorsedAndExtDirs) { if (match_option(option, "-Djava.endorsed.dirs=", &tail)) { // abort if -Djava.endorsed.dirs is set jio_fprintf(defaultStream::output_stream(), "-Djava.endorsed.dirs will not be supported in a future release.\n" "Refer to JEP 220 for details (http://openjdk.java.net/jeps/220).\n"); return JNI_EINVAL; } if (match_option(option, "-Djava.ext.dirs=", &tail)) { // abort if -Djava.ext.dirs is set jio_fprintf(defaultStream::output_stream(), "-Djava.ext.dirs will not be supported in a future release.\n" "Refer to JEP 220 for details (http://openjdk.java.net/jeps/220).\n"); return JNI_EINVAL; } } if (!add_property(tail)) { return JNI_ENOMEM; } // Out of the box management support if (match_option(option, "-Dcom.sun.management", &tail)) { #if INCLUDE_MANAGEMENT FLAG_SET_CMDLINE(bool, ManagementServer, true); #else jio_fprintf(defaultStream::output_stream(), "-Dcom.sun.management is not supported in this VM.\n"); return JNI_ERR; #endif } // -Xint } else if (match_option(option, "-Xint", &tail)) { set_mode_flags(_int); // -Xmixed } else if (match_option(option, "-Xmixed", &tail)) { set_mode_flags(_mixed); // -Xcomp } else if (match_option(option, "-Xcomp", &tail)) { // for testing the compiler; turn off all flags that inhibit compilation set_mode_flags(_comp); // -Xshare:dump } else if (match_option(option, "-Xshare:dump", &tail)) { FLAG_SET_CMDLINE(bool, DumpSharedSpaces, true); set_mode_flags(_int); // Prevent compilation, which creates objects // -Xshare:on } else if (match_option(option, "-Xshare:on", &tail)) { FLAG_SET_CMDLINE(bool, UseSharedSpaces, true); FLAG_SET_CMDLINE(bool, RequireSharedSpaces, true); // -Xshare:auto } else if (match_option(option, "-Xshare:auto", &tail)) { FLAG_SET_CMDLINE(bool, UseSharedSpaces, true); FLAG_SET_CMDLINE(bool, RequireSharedSpaces, false); // -Xshare:off } else if (match_option(option, "-Xshare:off", &tail)) { FLAG_SET_CMDLINE(bool, UseSharedSpaces, false); FLAG_SET_CMDLINE(bool, RequireSharedSpaces, false); // -Xverify } else if (match_option(option, "-Xverify", &tail)) { if (strcmp(tail, ":all") == 0 || strcmp(tail, "") == 0) { FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, true); FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, true); } else if (strcmp(tail, ":remote") == 0) { FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, false); FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, true); } else if (strcmp(tail, ":none") == 0) { FLAG_SET_CMDLINE(bool, BytecodeVerificationLocal, false); FLAG_SET_CMDLINE(bool, BytecodeVerificationRemote, false); } else if (is_bad_option(option, args->ignoreUnrecognized, "verification")) { return JNI_EINVAL; } // -Xdebug } else if (match_option(option, "-Xdebug", &tail)) { // note this flag has been used, then ignore set_xdebug_mode(true); // -Xnoagent } else if (match_option(option, "-Xnoagent", &tail)) { // For compatibility with classic. HotSpot refuses to load the old style agent.dll. } else if (match_option(option, "-Xboundthreads", &tail)) { // Bind user level threads to kernel threads (Solaris only) FLAG_SET_CMDLINE(bool, UseBoundThreads, true); } else if (match_option(option, "-Xloggc:", &tail)) { // Redirect GC output to the file. -Xloggc:<filename> // ostream_init_log(), when called will use this filename // to initialize a fileStream. _gc_log_filename = strdup(tail); if (!is_filename_valid(_gc_log_filename)) { jio_fprintf(defaultStream::output_stream(), "Invalid file name for use with -Xloggc: Filename can only contain the " "characters [A-Z][a-z][0-9]-_.%%[p|t] but it has been %s\n" "Note %%p or %%t can only be used once\n", _gc_log_filename); return JNI_EINVAL; } FLAG_SET_CMDLINE(bool, PrintGC, true); FLAG_SET_CMDLINE(bool, PrintGCTimeStamps, true); // JNI hooks } else if (match_option(option, "-Xcheck", &tail)) { if (!strcmp(tail, ":jni")) { #if !INCLUDE_JNI_CHECK warning("JNI CHECKING is not supported in this VM"); #else CheckJNICalls = true; #endif // INCLUDE_JNI_CHECK } else if (is_bad_option(option, args->ignoreUnrecognized, "check")) { return JNI_EINVAL; } } else if (match_option(option, "vfprintf", &tail)) { _vfprintf_hook = CAST_TO_FN_PTR(vfprintf_hook_t, option->extraInfo); } else if (match_option(option, "exit", &tail)) { _exit_hook = CAST_TO_FN_PTR(exit_hook_t, option->extraInfo); } else if (match_option(option, "abort", &tail)) { _abort_hook = CAST_TO_FN_PTR(abort_hook_t, option->extraInfo); } else if (match_option(option, "-XX:+NeverTenure", &tail)) { // The last option must always win. FLAG_SET_CMDLINE(bool, AlwaysTenure, false); FLAG_SET_CMDLINE(bool, NeverTenure, true); } else if (match_option(option, "-XX:+AlwaysTenure", &tail)) { // The last option must always win. FLAG_SET_CMDLINE(bool, NeverTenure, false); FLAG_SET_CMDLINE(bool, AlwaysTenure, true); } else if (match_option(option, "-XX:+CMSPermGenSweepingEnabled", &tail) || match_option(option, "-XX:-CMSPermGenSweepingEnabled", &tail)) { jio_fprintf(defaultStream::error_stream(), "Please use CMSClassUnloadingEnabled in place of " "CMSPermGenSweepingEnabled in the future\n"); } else if (match_option(option, "-XX:+UseGCTimeLimit", &tail)) { FLAG_SET_CMDLINE(bool, UseGCOverheadLimit, true); jio_fprintf(defaultStream::error_stream(), "Please use -XX:+UseGCOverheadLimit in place of " "-XX:+UseGCTimeLimit in the future\n"); } else if (match_option(option, "-XX:-UseGCTimeLimit", &tail)) { FLAG_SET_CMDLINE(bool, UseGCOverheadLimit, false); jio_fprintf(defaultStream::error_stream(), "Please use -XX:-UseGCOverheadLimit in place of " "-XX:-UseGCTimeLimit in the future\n"); // The TLE options are for compatibility with 1.3 and will be // removed without notice in a future release. These options // are not to be documented. } else if (match_option(option, "-XX:MaxTLERatio=", &tail)) { // No longer used. } else if (match_option(option, "-XX:+ResizeTLE", &tail)) { FLAG_SET_CMDLINE(bool, ResizeTLAB, true); } else if (match_option(option, "-XX:-ResizeTLE", &tail)) { FLAG_SET_CMDLINE(bool, ResizeTLAB, false); } else if (match_option(option, "-XX:+PrintTLE", &tail)) { FLAG_SET_CMDLINE(bool, PrintTLAB, true); } else if (match_option(option, "-XX:-PrintTLE", &tail)) { FLAG_SET_CMDLINE(bool, PrintTLAB, false); } else if (match_option(option, "-XX:TLEFragmentationRatio=", &tail)) { // No longer used. } else if (match_option(option, "-XX:TLESize=", &tail)) { julong long_tlab_size = 0; ArgsRange errcode = parse_memory_size(tail, &long_tlab_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid TLAB size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, TLABSize, long_tlab_size); } else if (match_option(option, "-XX:TLEThreadRatio=", &tail)) { // No longer used. } else if (match_option(option, "-XX:+UseTLE", &tail)) { FLAG_SET_CMDLINE(bool, UseTLAB, true); } else if (match_option(option, "-XX:-UseTLE", &tail)) { FLAG_SET_CMDLINE(bool, UseTLAB, false); } else if (match_option(option, "-XX:+DisplayVMOutputToStderr", &tail)) { FLAG_SET_CMDLINE(bool, DisplayVMOutputToStdout, false); FLAG_SET_CMDLINE(bool, DisplayVMOutputToStderr, true); } else if (match_option(option, "-XX:+DisplayVMOutputToStdout", &tail)) { FLAG_SET_CMDLINE(bool, DisplayVMOutputToStderr, false); FLAG_SET_CMDLINE(bool, DisplayVMOutputToStdout, true); } else if (match_option(option, "-XX:+ErrorFileToStderr", &tail)) { FLAG_SET_CMDLINE(bool, ErrorFileToStdout, false); FLAG_SET_CMDLINE(bool, ErrorFileToStderr, true); } else if (match_option(option, "-XX:+ErrorFileToStdout", &tail)) { FLAG_SET_CMDLINE(bool, ErrorFileToStderr, false); FLAG_SET_CMDLINE(bool, ErrorFileToStdout, true); } else if (match_option(option, "-XX:+ExtendedDTraceProbes", &tail)) { #if defined(DTRACE_ENABLED) FLAG_SET_CMDLINE(bool, ExtendedDTraceProbes, true); FLAG_SET_CMDLINE(bool, DTraceMethodProbes, true); FLAG_SET_CMDLINE(bool, DTraceAllocProbes, true); FLAG_SET_CMDLINE(bool, DTraceMonitorProbes, true); #else // defined(DTRACE_ENABLED) jio_fprintf(defaultStream::error_stream(), "ExtendedDTraceProbes flag is not applicable for this configuration\n"); return JNI_EINVAL; #endif // defined(DTRACE_ENABLED) #ifdef ASSERT } else if (match_option(option, "-XX:+FullGCALot", &tail)) { FLAG_SET_CMDLINE(bool, FullGCALot, true); // disable scavenge before parallel mark-compact FLAG_SET_CMDLINE(bool, ScavengeBeforeFullGC, false); #endif } else if (match_option(option, "-XX:CMSParPromoteBlocksToClaim=", &tail)) { julong cms_blocks_to_claim = (julong)atol(tail); FLAG_SET_CMDLINE(uintx, CMSParPromoteBlocksToClaim, cms_blocks_to_claim); jio_fprintf(defaultStream::error_stream(), "Please use -XX:OldPLABSize in place of " "-XX:CMSParPromoteBlocksToClaim in the future\n"); } else if (match_option(option, "-XX:ParCMSPromoteBlocksToClaim=", &tail)) { julong cms_blocks_to_claim = (julong)atol(tail); FLAG_SET_CMDLINE(uintx, CMSParPromoteBlocksToClaim, cms_blocks_to_claim); jio_fprintf(defaultStream::error_stream(), "Please use -XX:OldPLABSize in place of " "-XX:ParCMSPromoteBlocksToClaim in the future\n"); } else if (match_option(option, "-XX:ParallelGCOldGenAllocBufferSize=", &tail)) { julong old_plab_size = 0; ArgsRange errcode = parse_memory_size(tail, &old_plab_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid old PLAB size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, OldPLABSize, old_plab_size); jio_fprintf(defaultStream::error_stream(), "Please use -XX:OldPLABSize in place of " "-XX:ParallelGCOldGenAllocBufferSize in the future\n"); } else if (match_option(option, "-XX:ParallelGCToSpaceAllocBufferSize=", &tail)) { julong young_plab_size = 0; ArgsRange errcode = parse_memory_size(tail, &young_plab_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid young PLAB size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, YoungPLABSize, young_plab_size); jio_fprintf(defaultStream::error_stream(), "Please use -XX:YoungPLABSize in place of " "-XX:ParallelGCToSpaceAllocBufferSize in the future\n"); } else if (match_option(option, "-XX:CMSMarkStackSize=", &tail) || match_option(option, "-XX:G1MarkStackSize=", &tail)) { julong stack_size = 0; ArgsRange errcode = parse_memory_size(tail, &stack_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid mark stack size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, MarkStackSize, stack_size); } else if (match_option(option, "-XX:CMSMarkStackSizeMax=", &tail)) { julong max_stack_size = 0; ArgsRange errcode = parse_memory_size(tail, &max_stack_size, 1); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid maximum mark stack size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, MarkStackSizeMax, max_stack_size); } else if (match_option(option, "-XX:ParallelMarkingThreads=", &tail) || match_option(option, "-XX:ParallelCMSThreads=", &tail)) { uintx conc_threads = 0; if (!parse_uintx(tail, &conc_threads, 1)) { jio_fprintf(defaultStream::error_stream(), "Invalid concurrent threads: %s\n", option->optionString); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, ConcGCThreads, conc_threads); } else if (match_option(option, "-XX:MaxDirectMemorySize=", &tail)) { julong max_direct_memory_size = 0; ArgsRange errcode = parse_memory_size(tail, &max_direct_memory_size, 0); if (errcode != arg_in_range) { jio_fprintf(defaultStream::error_stream(), "Invalid maximum direct memory size: %s\n", option->optionString); describe_range_error(errcode); return JNI_EINVAL; } FLAG_SET_CMDLINE(uintx, MaxDirectMemorySize, max_direct_memory_size); } else if (match_option(option, "-XX:+UseVMInterruptibleIO", &tail)) { // NOTE! In JDK 9, the UseVMInterruptibleIO flag will completely go // away and will cause VM initialization failures! warning("-XX:+UseVMInterruptibleIO is obsolete and will be removed in a future release."); FLAG_SET_CMDLINE(bool, UseVMInterruptibleIO, true); #if !INCLUDE_MANAGEMENT } else if (match_option(option, "-XX:+ManagementServer", &tail)) { jio_fprintf(defaultStream::error_stream(), "ManagementServer is not supported in this VM.\n"); return JNI_ERR; #endif // INCLUDE_MANAGEMENT #if INCLUDE_JFR } else if (match_jfr_option(&option)) { return JNI_EINVAL; #endif } else if (match_option(option, "-XX:", &tail)) { // -XX:xxxx // Skip -XX:Flags= since that case has already been handled if (strncmp(tail, "Flags=", strlen("Flags=")) != 0) { if (!process_argument(tail, args->ignoreUnrecognized, origin)) { return JNI_EINVAL; } } // Unknown option } else if (is_bad_option(option, args->ignoreUnrecognized)) { return JNI_ERR; } } // PrintSharedArchiveAndExit will turn on // -Xshare:on // -XX:+TraceClassPaths if (PrintSharedArchiveAndExit) { FLAG_SET_CMDLINE(bool, UseSharedSpaces, true); FLAG_SET_CMDLINE(bool, RequireSharedSpaces, true); FLAG_SET_CMDLINE(bool, TraceClassPaths, true); } // Change the default value for flags which have different default values // when working with older JDKs. #ifdef LINUX if (JDK_Version::current().compare_major(6) <= 0 && FLAG_IS_DEFAULT(UseLinuxPosixThreadCPUClocks)) { FLAG_SET_DEFAULT(UseLinuxPosixThreadCPUClocks, false); } #endif // LINUX fix_appclasspath(); return JNI_OK; }
可以看到无论是 -agentlib
还是-agentpath
还是-javaagent
都会执行 add_init_agent
函数,而这个函数就是一个目的:构建Agent Library链表。也就是说将我们vm中传入的jar路径以及后边的参数存放起来(放到了 _agentList
链表中),然后 待后续使用。
创建JVM并调用create_vm_init_agents函数
从注释上可以看出有一个转换 -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 函数,并执行
通过 lookup_on_load 来查找 libinstrument.so 文件以及他的 Agent_OnLoad 方法
这里首先是根据 name 去构建动态链接文件(win中是 ddl,linux下是so)的名称,这就是为什么我们传入的是 instrument 而真正执行的动态链接文件是 libinstrument.dylib的原因
之后就是加载动态链接文件,然后就是寻找 OnLoad 也就是上边提到的 find_agent_function,最终将会找到的动态链接文件中的Agent_OnLoad 方法。
到此,寻找动态链接库以及执行动态链接库中的方法就分析完了。
找到 lib instrument.so 的真正实现 InvocationAdapter.c
实际上 libinstrument.so 这个动态链接库的实现是位于java/instrumentat/share/native/libinstrument 入口的InvocationAdapter.c
在上边的create_vm_init_agents 函数中我们查找并执行了动态链接库 lib instrument.so 中的 Agent_OnLoad函数,而这个函数最终会执行到 InvocationAdapter.c 中的 Agent_OnLoad中
执行 Agent_OnLoad 函数
创建与初始化 JPLISAgent
在 createNewJPLISAgent 中 创建了一个JPLISAgent(Java Programming Language Instrumentation Services Agent), 并且从 Vm环境中获取了 jvmtiEnv指针,用于后续的操作, jvmtiEnv 是一个很重要的指针(在 JVMTI 运行时,通常一个JVMTI Agent 对应一个 jvmtiEnv)。
其中我们比较关注的一步就是 初始化JPLISAgent
初始化 JPLISAgent 做了两件我们比较关注的事
- 监听VMinit 初始化事件
- 在监听到VMinit 事件后,设置 eventHandlerVMinit 回调函数。而在这里,本质上只是设置监听的事件(VM初始化),真正触发这个事件并执行的是在
Threads::create_vm
中的post_vm_initialized
。
接下来就是通过post_vm_initialized来执行 (在initializeJPLISAgent中)提前设置好的vm初始化回调事件即:eventHandlerVMInit
。
执行 eventHandlerVMInit 方法
eventHandlerVMInit方法比较重要
执行 processJavaStart 函数
eventHandlerVMInit
中的processJavaStart
,从名字上来看也很明了就是启动Java相关的程序。接下来我们会发现 越看越离java近。
通过阅读 processJavaStart 代码,我们知道这里边首先
- 创建 (sun.instrument.InstrumentationImpl)类的实例
- 监听&开启 ClassFileLoadHook事件,注册回调函数最终此回调函数会调用到:ClassFileTransformer 的 transform。
- 加载 java agent 并调用 permain方法(会把 Instrumentation类实例和 agent 参数传入 permain 方法中去),permain 中会将ClassFileTransformer 的实现添加进 Instrumentation 类的实例中去
开启并监听 ClassFileLoadHook 事件 -> setLivePhaseEventHandlers
而其中的第二步即 :监听&开启 ClassFileLoadHook 事件,里边的操作比较重要。
上边这个函数会设置 ClassFileLoadHook 的处理器,即类加载时的回调处理器 eventHandlerClassFileLoadHook
但是有一点我们要清楚,这里只是设置回调函数,并没有真正执行eventHandlerClassFileLoadHook的内容,因为此时还不到类加载阶段,切记这一点
在这个eventHandlerClassFileLoadHook里边会最终调用(注意不是此时调用,而是类加载时)到我们的 jdk中的ClassFileTransformer
接口的transform
方法
设置类加载时的回调函数处理器:eventHandlerClassFileLoadHook
上边这个 eventHandlerClassFileLoadHook 方法就是监听到类加载时的处理逻辑。其中的transformClassFile
会执行到我们的java代码。
调用到 java 代码的地方 -> transformClassFile
找到将被调用(注意不是此时调用)的 java 代码!!!(InstrumentationImpl 类的 transform 方法)
而上边这个(transformedBufferObject = (*jnienv)->CallObjectMethod(n多个参数)
)这段代码最终就会调到jdk中InstrumentationImpl
类的的transform
方法,如下:
在开启监听类加载事件 并 注册完类加载时的回调函数后,进行下边逻辑
加载 java agent 并调用 permain方法 -> startJavaAgent
调用我们 MAINFEST.MF Premain-Class 类中的 premain 方法并传入参数(包括启动时 -javaagent:xxjava.jar=option1=value1=option2=value2 传入的参数和 Instrumentation 的实例对象)
调用到 jdk代码 -> sun.instrument.InstrumentationImpl的 loadClassAndCallPremain
注意:loadClassAndCallPremain
中会调用loadClassAndStartAgent
方法
调用到我们MAINFEST.MF
文件中-> Premain-Class
类中的premain
方法(我们自己开发的代码)
loadClassAndStartAgent
最终会通过反射执行我们在MAINFEST.MF中指定的Premain-Class类里边的premain方法,值的注意的是:在premain方法中其实只是往 InstrumentationImpl
实例中添加了我们自己定义的类转换器(比如我的DefineTransformer类),还没有真正的执行DefineTransformer
的transform
函数
那么什么时候会执行(或者说 回调,这个词更符合此函数的调用动作)到我的 DefineTransformer
类中的tranform
方法去修改(Retransform)
或者 重新定义(Redefine)
类呢?那肯定是类加载时啊,上边我们说过很多遍了!
加载类的入口:systemDictionary.cpp -> load_instance_class
因为我们自己编写的类都是要通过系统类加载器加载的,所以会走到这个系统类加载
类加载时回调
在premain 中设置的转换器,即我们自定义 transformer 类的 transform 方法。
注意我们 自定义Transformer 类实现了 java.lang.instrument.ClassFileTransformer
接口的transform
方法!所以才会被调用到Transformer
类的transform
方法!这一点要明白!
继续跟进load_calassfile中的 parseClassFile方法: ps: 这个方法巨长,至少有600多行,类加载的主要逻辑就在这里边了,感兴趣可以去看看完整的,这里我们不粘完整版本了,只保留我们感兴趣的,调用类加载时候的钩子函数片段,代码如下:
post_class_file_load_hook:
Post_all_envs:
上边方法post_to_env中的这段:
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方法!!
将修改后的字节码保存到类文件流中去
在调用完DefineTransformer类的transform方法后,从上边可以看到返回了修改后的字节码,需要将修改后的类数据添加到类文件流,使得修改后的内容生效呀(最终加载到元空间的是我们在DefineTransformer类transform方法 修改后的内容),所以就有了下边的代码:
执行加载
后边的逻辑: -> 链接
(验证,准备,解析)-> 初始化
-> 使用
(如new or 反射 等等)
在 初始化这一步之后,类的元数据
被保存到了元空间(1.8后引入的)中,之后我们就可以愉快的使用了,比如new 或者反射等等根据类元数据创建实例这类行为,或者访问类的元数据比如 类.class 等等操作。
静态加载图解
上图简单语言概括下:
- 通过 main函数启动java程序
- create_vm 开始
- create_vm 函数执行完毕,开始类加载工作
4. Java Agent 动态加载 演示、图解、源码分析
动态加载相较于静态加载,会更灵活一点。
动态加载demo实现与演示
想要达到的效果
让 Integer.valueOf(int i)每次都装箱,不从 INtegerCache数组中取,也就是要达到 -127-128 两个 Integer 对象之间的对比也会返回 false)
我们知道如果生命了两个局部变量:( Integer i1=20;
和Integer i2=20;
),编译为class后将会被Integer.valueOf(int i);
方法包装,去 ==
比较时会返回true,这个原因是因为当i 在-128-127范围内时,valueOf不会将i装箱,而是从缓存数组中取对应索引的Integer对象,相同的值取得是相同索引位置的对象 == 比较时自然是相等
我们此处的案例想要的目的是:i1和i2去==比较时是不相等的,想要达到这个目的就得修改Integer.valueOf(int i);
方法的实现,将-128-127的int值都装箱,这样的话 只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象
修改前的 Integer.value(int i); 代码
修改后的 Integer.value(int i); 代码
public static Integer valueOf(int i) { return new Integer(i); }
编写agent jar 的逻辑实现
基本上编写一个 agent jar 需要三个内容
编写agentmain方法(即加载agent的入口)