关于Java Agent的使用、工作原理、及hotspot源码解析

关于Java Agent的使用、工作原理、及hotspot源码解析

本文涉及到的知识点

  1. JVMTI(Java Virtual Machine Tool Interface)
  2. JVMTIAgent
  3. Java Agent
  4. Java类加载机制
  5. unix套接字
  6. 信号机制(signal)
  7. hotspot源码
  8. 动态链接库文件(linux中是.so,win中是.dll结尾)
  9. JNI(java native interface)
  10. 字节码修改(本文使用的是javassist)
  11. 钩子(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.instrumentjava.lang.instrumentcom.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 方法之前执行。这一机制使得我们可以修改应用程序的类或执行其他初始化任务,这种机制对于性能监控代码分析审计增强 等场景非常有用

  1. 编写Agent代码: 开发一个Java类,实现 premain 方法并在其中将类转换的实现类添加到 Instrumentation 实例。这个方法是静态加载Agent的入口点,premian将在vm初始化时被调用。 编写转换增强(使用字节码工具比如javaassist 或ASM )逻辑 需要实现 ClassFileTransformer 类的transform方法,此方法在vm初始化(VMInit)阶段被 注册,在类加载时被 调用
  2. 打包Agent: 将 Agent 类和可能依赖的其他类打包成一个JAR文件。在Agent JAR的 MANIFEST.MF 文件中,必须要有 Premain-Class 属性,该属性的值是包含 premain 方法的类的全限定名。(一般我们通过maven打包插件来打包Agent Jar包,同样的,MANIFEST.MF文件中的内容也是通过插件来生成的
  3. 启动被插桩程序时指定Agent: 在启动被插桩程序时,通过添加 -javaagent:/path/to/youragent.jar 参数来指定Agent JAR。如果需要传递参数给Agent,可以在JAR路径后添加 = 符号和参数字符串,如-javaagent:/path/to/youragent.jar=config1=value1,config2=value2

动态加载

即在 JVM 运行应用程序时任意时刻加载,在 JVM 运行时加载 Agent,这通常通过使用 JDK 的 Attach API实现(本质上是使用 unix 套接字实现了同一机器不同进程间的通信)。这要求 Agent 实现 agentmain 方法,该方法可以在 java 应用程序运行过程中任意时刻被调用。

动态加载Java Agent主要依赖于Java Instrumentation API的 agentmain 方法和 Attach API。具体步骤如下:

  1. 准备Agent JAR: 与静态加载相同,需要准备一个包含 agentmain 方法的Agent JAR文件。agentmain 方法是动态加载 Agent 时由 JVM 调用的入口点。该 JAR 文件还需要在其 MANIFEST.MF 中声明 Agent-Class 属性,指向包含 agentmain 方法的类。编写转换增强(使用字节码工具比如javaassist 或ASM )逻辑 需要实现 ClassFileTransformer 类的 transform方法,与静态加载不同,此方法的调用需要通过 inst.retransformClasses(“要重新加载的类”); 来触发。
  2. 使用Attach API: Attach API允许一个运行中的Java进程连接(通过UNIX套接字)到另一个Java进程。一旦连接,它可以用来加载Agent JAR。这通常通过使用 com.sun.tools.attach.VirtualMachine 类实现,该类提供了附加到目标JVM进程并加载Agent的方法
  3. 加载Agent: 通过Attach API附加到目标JVM后,可以指定Agent JAR路径并调用 loadAgentloadAgentLibrary 方法来加载并初始化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 的主要功能&使用

功能

  1. 事件通知:JVMTI 允许工具通过事件获取 JVM 内发生的特定情况的通知,如线程启动/结束、类加载/卸载、方法进入/退出等。
  2. 线程管理:它提供了监控和管理 Java 程序中线程状态的能力。
  3. 堆和垃圾回收:JVMTI 支持查询堆信息、监控垃圾回收事件,以及在某些条件下控制垃圾回收的执行。
  4. 调试支持:JVMTI 为调试器提供了丰富的接口,支持断点、单步执行、字段访问/修改等调试功能。
  5. 性能监测:提供了监视和分析 JVM 性能的工具,如获取方法执行时间、内存使用情况等。

场景

  1. 开发调试工具:利用 JVMTI 提供的调试支持,开发强大的调试工具,比如 idea, eclipse等等。
  2. 性能分析:构建性能分析工具来识别 Java 应用的性能瓶颈。
  3. 监控工具:创建监控工具来实时监视 JVM 的健康状况和性能指标。
  4. 覆盖率分析:通过跟踪类和方法的加载与执行,帮助生成代码覆盖率报告。

运行时监控&性能分析类:

  • 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事件回调,并在回调函数中简单地打印被加载的类的名称(注意: 此处小案例使用了启动时静态加载。)

  1. 创建 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;
}

  1. 编译 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 结尾)

  1. 运行 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 做了两件我们比较关注的事

  1. 监听VMinit 初始化事件
  2. 在监听到VMinit 事件后,设置 eventHandlerVMinit 回调函数。而在这里,本质上只是设置监听的事件(VM初始化),真正触发这个事件并执行的是在 Threads::create_vm 中的 post_vm_initialized

接下来就是通过post_vm_initialized来执行 (在initializeJPLISAgent中)提前设置好的vm初始化回调事件即:eventHandlerVMInit

执行 eventHandlerVMInit 方法

eventHandlerVMInit方法比较重要

执行 processJavaStart 函数

eventHandlerVMInit中的processJavaStart,从名字上来看也很明了就是启动Java相关的程序。接下来我们会发现 越看越离java近。

通过阅读 processJavaStart 代码,我们知道这里边首先

  1. 创建 (sun.instrument.InstrumentationImpl)类的实例
  2. 监听&开启 ClassFileLoadHook事件,注册回调函数最终此回调函数会调用到:ClassFileTransformer 的 transform。
  3. 加载 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类),还没有真正的执行DefineTransformertransform函数

那么什么时候会执行(或者说 回调,这个词更符合此函数的调用动作)到我的 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 等等操作。

静态加载图解

上图简单语言概括下:

  1. 通过 main函数启动java程序
  2. create_vm 开始
  3. 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的入口)

全部评论

相关推荐

评论
1
2
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务