掘金 后端 ( ) • 2024-04-25 16:00

前言

之前写了一篇关于链路追踪的文章: 自实现分布式链路追踪 方案&实践 ,期间提到过可以使用ttl(全称:transmittable-thread-local) 去解决线程间链路id丢失的问题,而其中无侵入的使用方式:agent 插桩技术深深的吸引了我,谁曾想一入 agent 深似海,我差点出不来了😂😂😂。

说明: 本文很长,长到超出了掘金编辑器的限制字符数 10万,所以我在最后边只是图解,没有更多的文字和代码描述了,本文知识点较多,如果没接触过agent那必然大概率会懵(大部分知识点讲解完后,我都会配个图来总结归纳加强理解)。当你一点点去理解尝试后相信会有所收获,另外水平有限不对地方请指导。

本文大概结构:

  • 前置节:简单认识 ->JVMTI,Java Agent,JVMTIAgent,libinstrument.so (先混个脸熟)
  • 第一节:Java Agent介绍与(静/动)态加载方式描述+图解
  • 第二节:JVMTI介绍,功能&使用场景以及c语言自实现一个JVMTIAgent
  • 第三节:Java Agent 静态加载demo+源码分析+图解
  • 第四节:Java Agent 动态加载demo+源码分析+图解

本文涉及到的知识点:

  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. 字节码修改(本文使用的是 javaassist之前我的一篇文章有详细介绍:Javassist使用教程【译】
  11. 钩子(hook)机制:在编程中这个非常重要,不管是底层(如linux)还是上层框架(如spring),此机制都会给软件带来很大的扩展空间和灵活性,是编程中非常常见的一种技术,在下文中回调函数其实就是指的钩子函数,钩子是机制,回调是动作,本文中你叫他钩子函数或者回调函数都是一个意思。

0、前置说明

在开始之前,我们先来了解几个重要的内容,先对这些东西有个大体概念。

  • JVMTI: (全称: Java Virtual Machine Tool Interface)是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展,JVMTI源码在jdk8/jdk8u/jdk/src/share/javavm/export/jvmti.h 这个文件中,截图如下: image.png

  • Java Agent: 可以使用Java语言编写的一种agent,编写他(后边会讲到)的话会直接使用到jdk中的 Instrumentation API(在sun.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语言编写的插桩服务提供支持的。(在这里多出来个词叫 插桩,知道的就罢了,不知道的话姑且可以简单对等理解为:AOP中的增强)。下边是我安装的openJdk11上的libinstrument.so文件,可以看到他存在于我的JAVA_HOME/lib/目录下,其中就包含了 Agent_OnLoadAgent_OnAttachAgent_OnUnload三个我们比较关注的函数,截图如下: image.png 当我们静态加载agent jar(启动时添加vm参数 -javaagent:xxxjar包路径的方式)时Agent_OnLoad会调用到我们的premain方法,当我们动态加载(JVM的attach机制,通过发送load命令来加载)时Agent_OnAttach会调用到我们的agentmian方法。也许你现在不明白这个,但是当你看了下边的第三&四节源码后你就能串起来了。

  • Instrumentation API: 为Java Agent提供了一套Java层面的接口,它是在Java 5开始引入的,旨在为Java开发者提供一种标准方式来动态修改类的行为以及做增强操作,部分示例:image.png

  • JVMTIAgent: 是一个动态链接库,利用JVMTI暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:

    • Agent_OnLoad函数: 会在静态加载agent jar时调用。
    • Agent_OnAttach函数: 如果agent不是在启动时加载的,而是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载,则在加载过程中会调用该函数。
    • Agent_OnUnload函数: 在agent卸载时调用。
    • 更强大的功能: 在我们使用Java Agent时不管是静态加载还是动态加载,其实实现的功能比较有限,基本上也就是下边这些:
      • 静态加载可以实现:类加载时修改(transform)/(redefine)重定义类字节码
      • 动态加载可以实现:运行时修改类字节码,dump线程堆栈信息,获取系统配置等。动态加载实现的功能 完整的无非就是下边这几个:image.png
    • 而如果你直接去使用c编写一个JVMTIAgent, 那能实现的功能就比较多了,你可以根据需要实现JVMTI预留出的每一个钩子函数,从而在指定的时机来让jvm加载你的逻辑以达到你的目的,这就是钩子函数的灵活之处。

以上几个知识点之间的关系图如下: ps:牢记这几个知识点之间的关系与各自的功能,会使我们理解本文起到事半功倍的效果!!!image.png

接下来,我们深入展开讲解下以上这些知识点。

1、Java Agent

Java Agent 是什么?

Java Agent是Java平台提供的一种特殊机制,它允许开发者 在Java应用程序 (被jvm加载 / 正在被jvm运行) 注入我们指定的字节码。这种技术被广泛应用于 功能增强监控性能分析调试信息收集等多种场景 , Java Agent 依赖于 instrument 这个特殊的 JVMTIAgent(Linux下对应的动态库是libinstrument.so),还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),专门为Java语言编写的插桩服务提供支持的, Java Agent有两种加载时机,分别是:

Java Agent 加载方式

静态加载

  1. 静态加载即 JVM启动时加载,在JVM启动时通过命令行参数-javaagent:path/to/youragent.jar指定Agent的 jar包。这要求Agent的入口类(即agent.jar包中的META-INF->MAINIFEST.MF文件中的Premain-Class对应的类)实现premain方法,该方法会在应用程序的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

动态加载

  1. 动态加载即 在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回调机制,这些框架也许不能诞生或者需要使用其他更复杂的技术。既然回调机制如此重要,那么都有哪些回调呢?让我们从源码中获取这个内容,如下:

以下是 hotspot 的 JVMTI 中定义的一系列回调函数,(暂时我们定义这段代码片段为 code1,以便后边引用 ):

源码在: /jdk8u/jdk/src/share/javavm/export/jvmti.h
    /* Event Callback Structure */
    /* 为了方便,我直接把代码和注释搞一行里了。 */
typedef struct {             
    /*   50 : VM Initialization Event jvm初始化 本文后续会讲解到这个,就是在这一步 
    设置的类加载时的回调函数和调用的premain方法  */ 
    jvmtiEventVMInit VMInit; 
    
    jvmtiEventVMDeath VMDeath;/*   51 : VM Death Event jvm销毁 */
    jvmtiEventThreadStart ThreadStart;/*   52 : Thread Start 线程启动 */
    jvmtiEventThreadEnd ThreadEnd;/*   53 : Thread End 线程结束 */
    jvmtiEventClassFileLoadHook ClassFileLoadHook;/* 54:ClassFileLoadHook类文件加载类加载时会调用*/
    jvmtiEventClassLoad ClassLoad; /*   55 : Class Load  */
    jvmtiEventClassPrepare ClassPrepare;/*   56 : Class Prepare */
    jvmtiEventVMStart VMStart; /*   57 : VM Start Event */                 
    jvmtiEventException Exception; /*   58 : Exception */
    jvmtiEventExceptionCatch ExceptionCatch;/*   59 : Exception Catch */
    jvmtiEventSingleStep SingleStep;/*   60 : Single Step */
    jvmtiEventFramePop FramePop;/*   61 : Frame Pop */
    jvmtiEventBreakpoint Breakpoint;/*   62 : Breakpoint */
    jvmtiEventFieldAccess FieldAccess;/*   63 : Field Access */
    jvmtiEventFieldModification FieldModification;/*   64 : Field Modification */
    jvmtiEventMethodEntry MethodEntry;/*   65 : Method Entry */                       
    jvmtiEventMethodExit MethodExit;/*   66 : Method Exit */
    jvmtiEventNativeMethodBind NativeMethodBind;/*   67 : Native Method Bind */
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;/*   68 : Compiled Method Load */   
    jvmtiEventCompiledMethodUnload CompiledMethodUnload; /*   69 : Compiled Method Unload */                    
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;/*   70 : Dynamic Code Generated */
    jvmtiEventDataDumpRequest DataDumpRequest;/*   71 : Data Dump Request */
    jvmtiEventReserved reserved72;
    jvmtiEventMonitorWait MonitorWait;/*   73 : Monitor Wait */                    
    jvmtiEventMonitorWaited MonitorWaited;/*   74 : Monitor Waited */                   
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;/*   75 : Monitor Contended Enter */         
    jvmtiEventMonitorContendedEntered MonitorContendedEntered; /*   76 : Monitor Contended Entered */
    jvmtiEventReserved reserved77;/*   77 */
    jvmtiEventReserved reserved78;/*   78 */
    jvmtiEventReserved reserved79;/*   79 */
    jvmtiEventResourceExhausted ResourceExhausted;/*   80 : Resource Exhausted */       
    jvmtiEventGarbageCollectionStart GarbageCollectionStart; /*   81 : Garbage Collection Start */          
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;/*   82 : Garbage Collection Finish */
    jvmtiEventObjectFree ObjectFree;/*   83 : Object Free */        
    jvmtiEventVMObjectAlloc VMObjectAlloc;/*   84 : VM Object Allocation */
} jvmtiEventCallbacks;

基于上边code1的代码我们总结归类下大概是这样:(实际上本文的agent只是和ClassFileLoadHook以及 VMInit这俩有关,其他的我们了解即可,当然除了这俩之外我们也是可以在其他节点(下边规定的这些节点)扩展实现JVMTI的一系列回调函数,不过需要使用c实现)

VM 生命周期事件:

VMInit: 当虚拟机初始化时触发,在此时会注册类加载时的回调函数和调用的premain方法(在源码小节会说到)。

VMDeath: 当虚拟机终止之前触发。

VMStart: 在虚拟机启动期间,任何Java代码执行之前触发。

类加载事件:

ClassFileLoadHook:类加载时调用此钩子函数的实现ClassFileTransformer 的transform

ClassLoad: 类加载到虚拟机后触发。

ClassPrepare: 类所有静态初始化完成,所有静态字段准备好,且所有方法都已绑定后触发。

线程事件:

ThreadStart: 线程启动时触发。

ThreadEnd: 线程结束时触发。 ####方法执行事件: MethodEntry: 进入方法时触发。

MethodExit: 退出方法时触发。

异常事件:

Exception: 方法执行过程中抛出异常时触发。

ExceptionCatch: 方法捕获到异常时触发。

监控和编译事件

MonitorContendedEnter: 线程尝试进入已被其他线程占用的监视器时触发。

MonitorContendedEntered: 线程进入已被其他线程占用的监视器后触发。

MonitorWait: 线程等待监视器的notify/notifyAll时触发。

MonitorWaited: 线程等待监视器的notify/notifyAll结束后触发。

CompiledMethodLoad: 方法被编译时触发。

CompiledMethodUnload: 编译的方法被卸载时触发。

字段访问和修改事件:

FieldAccess: 访问字段时触发。

FieldModification: 修改字段时触发。

其他事件:

GarbageCollectionStart: 垃圾收集开始时触发。

GarbageCollectionFinish: 垃圾收集完成时触发。

DataDumpRequest: 请求转储数据时触发。

这些事件回调为Java应用和工具提供了深入虚拟机内部操作的能力,从而能够进行更加精细的监控和调试。开发者可以根据需要注册监听特定的事件,本质上也就是我们说的开发者与JVM的 ”互动“

接下来我们看下JVMTI的主要功能,其实如果你看了上边的回调节点,基本上可以猜到他主要能干些啥,因为这些功能都是靠实现上边这些回调节点来开发的。

JVMTI 的主要功能&使用场景

功能:

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

场景:

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

文字描述你可能感觉不到什么,但是如果提到这些框架,你大概率会知晓其中的一个或者几个,而他们就是基于Java Agent 实现,而Java Agent本质上是需要依赖JVMTI的,所以可以说这些大名鼎鼎的框架 直接/间接 上都是 依赖了JVMTI,比如下边这些:

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

  • VisualVM:是JDK自带的一个用于Java程序性能分析的可视化工具,通过他可以获取应用程序的,堆,内存,线程,cpu,快照等等运行时信息。
  • JProfiler:和VisualVM类似,也是能获取Java应用程序以及jvm的各种信息。
  • BTrace:是一个监控&追踪工具,可以监控程序状态,获取运行时数据信息,如方法返回值,参数,调用次数,全局变量,调用堆栈等。
  • Arthas: 是阿里的一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率
  • Greys:是一个JVM进程执行过程中的异常诊断工具,可以在不中断程序执行的情况下轻松完成问题排查工作。其实他也是模仿了BTrace

热加载类:

  • HotSwapAgent:是一个免费的开源插件,它扩展了JVM内置的HotSwap机制的功能
  • reload:
  • JRebel:是一个商业化的Java热加载工具,它使开发者能够在不重启JVM的情况下,实时地重新加载改动后的类文件
  • spring-loaded:是一个开源的热加载工具,主要用于Spring框架,但也可以用于非Spring应用。
  • Spring Boot DevTools: 是 Spring Boot 的一个模块,提供了诸多功能其中包括热加载。

链路追踪类

  • skywalking:是一个开源的应用性能监控(APM)工具,主要用于监控、追踪、诊断分布式系统,特别是基于微服务、云原生和容器化(Docker, Kubernetes, Mesos)架构的大规模分布式系统。SkyWalking 提供全面的解决方案,包括服务性能监控、服务拓扑分析、服务和服务实例性能分析,以及对调用链路的追踪和诊断,可以看到他的功能很强大也很多,其中链路追踪只是他的一部分功能。
  • Pinpoint :也是一个链路追踪APM框架,支持java和php。

开发调试类:

  • IDEA 的 debug(这也是我们天天用的功能):比如我们在启动项目时,idea会自动加上这个jar,如下:image.png这个jar其实就负责IDEA与JVM之间的 通信,执行例如设置断点、暂停和恢复执行、修改字段值等调试指令,同时他还可以收集Java 应用运行状态的数据,例如方法调用、变量状态、线程信息等。这样我们在debug时就可以看到那么多的数据啦。注意: idea debug 其实不单单仅靠一个agent实现,他的实现是基于Java Platform Debugger Architecture (JPDA),即Java 平台调试架构,这个架构包含3部分 (JVMTI(JVM Tool Interface)、JDWP(Java Debug Wire Protocol)、JDI(Java Debug Interface))所以说我们启动项目时看到的 debuger-agent.jar 只是使用了JVMTI这部分。具体debug功能如何实现我们不过多展开了。
  • eclipse 的 debug这位功臣现在似乎用的不多了,但是我猜测它的debug肯定也是要依赖JVMTI的。

包括在我的链路追踪文章中使用 的ttl agent方式也是依赖了JVMTI。

当然,肯定还有很多我不知道的框架亦或者插件直接或者间接使用到了JVMTI,这里我们不过多讨论了。 上边简单介绍了JVMTI是什么,以及他的功能和使用场景,以及一些直接/间接使用到他的框架。下边我们就看看如何直接实现JVMTI Agent。

使用c编写一个JVMTIAgent,需要实现JVMTI的 ClassFileLoadHook 这个钩子函数

在JVMTI简介中我们看到很多JVMTI的回调节点,而这些函数的定义都在hotspot/jdk/src/share/javavm/jvmti.h 这个文件中,如下: image.png 可以看到有很多回调钩子(本文所讲的Java Agent其实只是用到了 类加载时的回调 这么一个函数),只要实现了这些钩子,jvm会在执行到这些钩子对应的时机,去勾取对应的实现。从而完成 开发者 与 jvm“互动”。 另外 JVMTI工作在更接近JVM核心的层面,提供了比Java Agent通过Instrumentation API更底层、更广泛的控制能力。例如,JVMTI可以用来实现复杂的调试器或性能分析工具,这些工具需要在JVM内部进行深入的操作,而这些操作可能超出了纯Java代码(即使是通过Instrumentation API)能够实现的范围,更多的情况是需要使用c/c++语言来实现。

比如说我们最常见的也是在本文要讲的,即,想在某个类的字节码文件读取之后类定义之前能修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那我们就可以实现一个回调函数赋给JvmtiEnv (JvmtiEnv是一个指针 指向JVMTI的数据结构,在JVMTI中每个agent都通过这个JvmtiEnv与JVM交互)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数里来了。 而有一点我们要知道,就是在Java的Instrumentation API引入之前(Java 5之前),想实现ClassFileLoadHook这个钩子函数(即在类字节码加载到JVM时进行拦截和修改)我们只能是编写原生代码也就是c/c++代码来实现(当然你可以使用代理或者覆盖类加载器的loadClass方法,这里我们不做讨论),而在Java 5之后引入了Instrumentation API ,所以我们能像现在这样,通过以下这种java代码实现, image.png 如果是Java 5之前?对不起,你只能是通过原生来实现也就是c/c++代码。

我们下边就给他使用c代码实现一个 JVMTI中 ClassFileLoadHook, 这个钩子函数中的逻辑比较简单,它演示了如何使用c语言设置ClassFileLoadHook事件回调,并在回调函数中简单地打印被加载的类的名称(注意: 此处小案例使用了启动时静态加载,如果要动态加载需要实现 Agent_OnAttach函数,这里我们不做演示)。步骤如下:

1. 创建JVMTI Agent:

创建一个名为ClassFileLoadHookAgent.c的C文件,用于实现JVMTI Agent:

#include <jvmti.h>
#include <stdio.h>
#include <stdlib.h>
// ClassFileLoadHook回调函数
void JNICALL ClassFileLoadHook(
    jvmtiEnv *jvmti_env,
   JNIEnv* jni_env,
   jclass class_being_redefined,
   jobject loader,
   const char* name,
   jobject protection_domain,
   jint class_data_len,
   const unsigned char* class_data,
   jint* new_class_data_len,
   unsigned char** new_class_data) {
   // 打印即将加载的类的名称
   if (name != NULL) {
       printf("使用c编写ClassFileLoadHook的实现_当前加载的类名称是: %s\n", name);
   }
}
// Agent_OnLoad,JVMTI Agent的入口点
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) >{
   jvmtiEnv *jvmti = NULL;
   jvmtiCapabilities capabilities;
   jvmtiEventCallbacks callbacks;
   jvmtiError err;
   // 获取JVMTI环境
   jint res = (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION_1_2);
   if (res != JNI_OK || jvmti == NULL) {
       printf("ERROR: Unable to access JVMTI Version 1.2 (%d)\n", res);
       return JNI_ERR;
   }

   // 设置所需的能力
   (void)memset(&capabilities, 0, sizeof(jvmtiCapabilities));
   capabilities.can_generate_all_class_hook_events = 1;
   err = (*jvmti)->AddCapabilities(jvmti, &capabilities);
   if (err != JVMTI_ERROR_NONE) {
       printf("ERROR: Unable to AddCapabilities (%d)\n", err);
       return JNI_ERR;
   }
   // 设置 ClassFileLoadHook 回调事件
   (void)memset(&callbacks, 0, sizeof(callbacks));
   callbacks.ClassFileLoadHook = &ClassFileLoadHook;
   err = (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));
   if (err != JVMTI_ERROR_NONE) {
       printf("ERROR: Unable to SetEventCallbacks (%d)\n", err);
       return JNI_ERR;
   }
   // 启用 ClassFileLoadHook 事件
   err = (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, >JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
   if (err != JVMTI_ERROR_NONE) {
       printf("ERROR: Unable to SetEventNotificationMode for ClassFileLoadHook >(%d)\n", err);
       return JNI_ERR;
   }
   return JNI_OK;
}

2. 编译Agent: 编译这个Agent需要依赖于你的操作系统和JDK安装路径。例如,在我的Linux (centos7) 上,则使用以下gcc命令来进行编译:

gcc -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -o >classfileloadhookagent.so ClassFileLoadHookAgent.c

这里${JAVA_HOME}是你JDK的安装目录,这条命令会生成一个名为classfileloadhookagent.so的共享库(动态链接库 linux中一般以 .so 结尾之前说过了)文件。

3. 运行Agent: 使用-agentpath参数将你的Agent附加到Java应用程序。并使用java命令执行编译后的class文件,如下:

java -agentpath:/usr/local/src/agent/classfileloadhookagent.so NativeCodeImplClassFileLoadHookTest

当Java应用程序运行时,每当类文件被加载前,你的ClassFileLoadHook回调函数将被触发,打印出即将加载的类的名称,接下来我们实操&演示下。

实操&演示

下面进行演示,如下:

(注意代码中是去掉包名的因为这样我们只需要 java NativeCodeImplClassFileLoadHookTest 就可以执行class文件了,有包名的话还得全限定所以我们就不加包名了)

image.png image.png image.png 可以看到通过在 ClassFileLoadHookAgent.c中实现函数 Agent_OnLoad并设置&开启回调事件ClassFileLoadHook,成功的让jvm在加载类时调用了回调函数,也就是执行了这段代码: printf("使用c编写ClassFileLoadHook的实现_当前加载的类名称是: %s\n", name); 看到这里 你会不通过java instrument api的方式编写JVMTI的回调了吗? 其他的回调函数其实也类似,这里我们只演示了 ClassFileLoadHook这个回调如何实现 。

上边我们讲解了Java Agent和JVMTI以及如何实现一个JVMTIAgent,到这里相信你已经有所了解,接下来我们就编写几个agent案例并分别分析他们的实现原理以及源码流程。让我们对 agent 的工作机制以及底层实现 有更深入的认识。

ps: 静态加载和动态加载区别还是比较大的,所以我打算把他们分开各说各的,以免混淆。

3、Java Agent 静态加载演示、图解、源码分析

静态加载demo实现与演示

(一些比较细的东西,我都放到代码注释中了,在代码外就不额外啰嗦了)

想要达到的效果

通过agent插桩的方式修改Date类的getTime()方法,使其返回的时间戳为:秒级别而不是毫秒级,如下是Date类的getTime方法一览: image.png

通过Instrument API和javaassist 编写插桩代码:

关于javaassist如果不了解的话,可以参考我的上一篇文章:Javassist使用教程【译】

package com.xzll.agent.config;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * @Author: 黄壮壮
 * @Date: 2023/3/3 09:15:21
 * @Description:
 */
public class JdkDateAgentTest {

   public static void premain(String args, Instrumentation inst) throws Exception {
      //调用addTransformer()方法对启动时所有的类(应用层)进行拦截
      inst.addTransformer(new DefineTransformer(), true);
   }
   static class DefineTransformer implements ClassFileTransformer {
      @Override
      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
         //操作Date类
         if ("java/util/Date".equals(className)) {
            CtClass clazz = null;
            System.out.println("对date执行插桩 【开始】");
            try {
               // 从ClassPool获得CtClass对象 (ClassPool对象是CtClass对象的容器,CtClass对象是类文件的抽象表示)
               final ClassPool classPool = ClassPool.getDefault();
               clazz = classPool.get("java.util.Date");
               //获取到java.util.Date类的 getTime方法
               CtMethod getTime = clazz.getDeclaredMethod("getTime");
               //(修改字节码) 这里对 java.util.Date.getTime() 方法进行了改写,先打印毫秒级时间戳,然后在return之前给他除以1000(变成秒级) 并返回。
               String methodBody = "{" +
                     "long currentTime = getTimeImpl();" +
                     "System.out.println(" 使用agent探针对Date方法进行修改并打印,当前时间【毫秒级】:"+currentTime );" +
                     "return currentTime/1000;" +
                     "}";
               getTime.setBody(methodBody);
               //通过CtClass的toBytecode(); 方法来获取 被修改后的字节码
               return clazz.toBytecode();
            } catch (Exception ex) {
               ex.printStackTrace();
            } finally {
               if (null != clazz) {
                  //调用CtClass对象的detach()方法后,对应class的其他方法将不能被调用。但是,你能够通过ClassPool的get()方法,
                  //重新创建一个代表对应类的CtClass对象。如果调用ClassPool的get()方法, ClassPool将重新读取一个类文件,并且重新创建一个CtClass对象,并通过get()方法返回
                  //如下所说:
                  //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                  clazz.detach();
               }
               System.out.println("对date执行插桩【结束】");
            }
         }
         return classfileBuffer;
      }
   }
}

配置打包时的方式和MAINFSET.MF数据在pom中

配置maven打包方式与数据 (我这里使用assembly打包),pom代码如下:

<build>
    <plugins>
       <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
             <source>11</source>
             <target>11</target>
          </configuration>
       </plugin>

       <!-- Maven Assembly Plugin -->
       <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-assembly-plugin</artifactId>
          <version>2.4.1</version>
          <configuration>
             <!-- 将所有的依赖全部打包进jar -->
             <descriptorRefs>
                <descriptorRef>jar-with-dependencies</descriptorRef>
             </descriptorRefs>
             <!-- MainClass in mainfest make a executable jar -->
             <archive>
                <manifestEntries>
                    <!--设置jar的作者和时间-->
                   <Built-By>黄壮壮</Built-By>
                   <Built-Date>${maven.build.timestamp}</Built-Date>

                   <!--指定premain方法(静态加载时会调用的方法)的入口类,也就是说告诉jvm, premain方法在哪个类中-->
                   <Premain-Class>com.xzll.agent.config.JdkDateAgentTest</Premain-Class>

                   <!--该属性设置为 true 时表示:允许已加载的类被重新转换(retransform)。这意味着 Java Agent 可以在运行时修改已经加载的类的字节码,而不需要重新启动应用或 JVM
                   注意,如果此属性设置为 false 在执行main方法且设置-jaavaagent.jar时,将会执行抛出异常 :java.lang.instrument ASSERTION FAILED ***: "result" with message agent load/premain call failed at src/java.instrument/share/native/libinstrument/JPLISAgent.c line: 422
                   -->
                   <Can-Retransform-Classes>true</Can-Retransform-Classes>

                   <!--该属性设置为 true 时表示:允许 Java Agent 在运行时重新定义(也就是完全替换)已加载的类的字节码,这里我们没用到这个暂时设置成false,用到时在打开-->
                   <Can-Redefine-Classes>false</Can-Redefine-Classes>

                   <!--该属性设置为 true 时表示:允许 Java Agent 在运行时动态地为 JNI (Java Native Interface) 方法设置前缀。这项能力主要用于修改或拦截对本地方法的调用,这里我们没用到也设置为false -->
                   <Can-Set-Native-Method-Prefix>false</Can-Set-Native-Method-Prefix>

                   <!--指定agentmain方法的入口类(动态加载时将会调用 agentmain方法)-->
                   <!--<Agent-Class>com.xzll.agent.config.MysqlFieldCryptByExecuteBodyAgent</Agent-Class>-->
                </manifestEntries>
                 
                <!--如果不在pom中设置以上manifestEntries 这些信息,那么也可以在手动建一个MANIFEST.MF文件在 src/main/resources/META-INF/目录中,并将这些信息手动写进文件,然后让assembly打包时使用我们自己手写的这个MANIFEST.MF文件(如下的 manifestFile 标签就是告诉插件使用我们自己写的MANIFEST.MF文件),但是那样容易出错所以我们最好是在pom中设置然后让assembly插件帮我们生成 -->
                <!--<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>-->

             </archive>
          </configuration>
          <executions>
             <!-- 配置执行器 -->
             <execution>
                <id>make-assembly</id>
                <!-- 绑定到package命令的生命周期上 -->
                <phase>package</phase>
                <goals>
                   <!-- 只运行一次 -->
                   <goal>single</goal>
                </goals>
             </execution>
          </executions>
       </plugin>
    </plugins>
</build>

使用mvn package 命令打包

image.png

解压jar 并查看/META-INF/MANIFEST.MF文件内容

使用命令解压jar:

unzip ~/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar -d ~/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencie

查看/META-INF/MANIFEST.MF文件内容: image.png

编写&执行main方法(使用-javaagent静态加载上边的agent jar包)

编写并执行main方法,这里我们很重要的一步就是在 vm参数中配置了 此内容:

-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar

也就是我们所说的: 静态加载image.png 看下效果: image.png 可以看到,在main方法启动时添vm参数(即:

-javaagent:/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar

)从而让jvm启动时(也即静态)加载我们编写的agent jar ,使得在执行main方法里的getTime方法时执行的是我们修改替换(transform)后的,修改后的 getTime 方法体内容是:

{
      long currentTime = getTimeImpl();
      System.out.println(" 使用agent探针对Date方法进行修改并打印,当前时间【毫秒级】:"+currentTime );
      return currentTime/1000;
}

因此让Date getTime()方法返回了秒级时间戳。,这就是所谓的 插桩。是不是有点aop的意思?

以上就是静态加载的demo了,虽然很简单,但是麻雀虽小五脏俱全了也算是,趁热打铁吧,下边我们就从 源码角度来逐步分析静态加载实现的流程与原理 ,注意 源码小节比较重要 ,看完源码,才会有恍然大悟的感觉。没错我就是这个感觉。

静态加载源码解析

解析启动时传入的vm参数

源码这一节我准备从源头说起,我们知道静态加载agent时我们必须使用-javaagent:xxx.jar 而我们就从这里说起,看看jvm到底是如何解析运作的,首先第一步传入的参数jvm得认识吧?所以就来到了 解析参数这一步,解析参数的入口在这里: image.png 接下来到 parse_each_vm_init_arg 这个里边,而这个函数的内容超级多,因为我们知道vm参数巨多,所以这个里边的代码也巨长,但是我们这里只关心-javaagent,其他的我们知道了解即可,

完整代码在: /hotspot/src/share/vm/runtime/arguments.cpp 中

jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args,
                                       SysClassPath* scp_p,
                                       bool* scp_assembly_required_p,
                                       Flag::Flags origin) {
.......略掉n多行代码.......
接下的参数有很多,随便举几个比较熟悉/听过的吧:
-Xbootclasspath
-Xmn
-Xms
-Xmx
-XX:MaxHeapSize=
-XX:ReservedCodeCacheSize
-XX:IncreaseFirstTierCompileThresholdAt
-XX:+CMSPermGenSweepingEnabled
-XX:+UseGCTimeLimit
-XX:TLESize
-XX:TLEThreadRatio
-XX:CMSParPromoteBlocksToClaim
-XX:CMSMarkStackSize
-XX:ParallelCMSThreads
-XX:MaxDirectMemorySize

//与agent相关的,可以看到 不管是 -agentlib 还是-agentpath还是-javaagent,
//最终都会执行到一个函数即:add_init_agent 
#endif // !INCLUDE_JVMTI
        add_init_library(name, options);
      }
    // -agentlib and -agentpath
    } else if (match_option(option, "-agentlib:", &tail) ||
          (is_absolute_path = match_option(option, "-agentpath:", &tail))) {
      if(tail != NULL) {
        const char* pos = strchr(tail, '=');
        size_t len = (pos == NULL) ? strlen(tail) : pos - tail;
        char* name = strncpy(NEW_C_HEAP_ARRAY(char, len + 1, mtInternal), tail, len);
        name[len] = '\0'; 

        char *options = NULL;
        if(pos != NULL) {
          size_t length = strlen(pos + 1) + 1;
          options = NEW_C_HEAP_ARRAY(char, length, mtInternal);
          jio_snprintf(options, length, "%s", pos + 1);
        }
#if !INCLUDE_JVMTI
#endif // !INCLUDE_JVMTI
        add_init_agent(name, options, is_absolute_path);
      }
    // -javaagent
    } else if (match_option(option, "-javaagent:", &tail)) {
#else
      if(tail != NULL) {
        size_t length = strlen(tail) + 1;
        char *options = NEW_C_HEAP_ARRAY(char, length, mtInternal);
        jio_snprintf(options, length, "%s", tail);
        
        //此处传入的 instrument 会被在前边加上 lib ,
        //在后边加上.so 也就是最终的 libinstrument.so 看到这个相信已经很熟悉了
        //这就是我们使用-javaagent时  底层所使用的 动态库文件名,该函数在上边有介绍,忘记的回去看看。
        add_init_agent("instrument", options, false); 
      }
.......略掉n多行代码.......
  //而这个里边就是很简单的一件事,即构建Agent Library链表,也就是说将我们vm中传入的jar路径以及后边的参数存放起来然后待后续使用。
  static AgentLibraryList _agentList;
  static void add_init_agent(const char* name, char* options, bool absolute_path)
    { _agentList.add(new AgentLibrary(name, options, absolute_path, NULL)); }
    

可以看到无论是 -agentlib还是-agentpath还是-javaagent 都会执行 add_init_agent 函数,而这个函数就是一个目的:构建Agent Library链表。也就是说将我们vm中传入的jar路径以及后边的参数存放起来(放到了 _agentList 链表中),然后 待后续使用

创建JVM并调用create_vm_init_agents函数

解析完参数后,就来到了创建并启动jvm的环节,创建并启动jvm做的工作很多,我只保留了和agent相关的代码,如下:

此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中


jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {

  ...略去n行代码
  
  // Convert -Xrun to -agentlib: if there is no JVM_OnLoad
  // Must be before create_vm_init_agents()
  if (Arguments::init_libraries_at_startup()) {
    convert_vm_init_libraries_to_agents();
  }
  // Launch -agentlib/-agentpath and converted -Xrun agents
  if (Arguments::init_agents_at_startup()) {
    create_vm_init_agents();
  }
  ...略去n行代码
}

从注释上可以看出有一个转换 -Xrun为 -agentlib 的操作,而-Xrun 是 Java 1.4 及之前版本用于加载本地库(native libraries)使用的,尤其是用于加载性能分析或调试工具的老旧方式。从 Java 1.5 开始,推荐使用 -agentlib 作为替代,这是因为 -agentlib 提供了更标准化和更简单的方式来加载和管理 Java Agent,有这个代码的存在是为了更好的向下兼容。这里我们知道这么个事就行了,重点关注下边的逻辑。即:create_vm_init_agents();,这个方法就是创建&初始化agent的入口方法了。此方法内容如下:

遍历agents链表并调用lookup_agent_on_load找到某个动态链接中的Agent_OnLoad函数,并执行

此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中

// Create agents for -agentlib:  -agentpath:  and converted -Xrun
// Invokes Agent_OnLoad
// Called very early -- before JavaThreads exist
void Threads::create_vm_init_agents() {
  extern struct JavaVM_ main_vm;
  AgentLibrary* agent;

  JvmtiExport::enter_onload_phase();

  for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
    //lookup_agent_on_load主要功能就是找到动态链接文件,然后找到里面的Agent_Onload方法并返回
    OnLoadEntry_t  on_load_entry = lookup_agent_on_load(agent);
    if (on_load_entry != NULL) {
      // Invoke the Agent_OnLoad function  在此处调用 上边找到的 动态链接库中的Agent_OnLoad 
      //方法!
      jint err = (*on_load_entry)(&main_vm, agent->options(), NULL);
      if (err != JNI_OK) {
        vm_exit_during_initialization("agent library failed to init", agent->name());
      }
    } else {
      vm_exit_during_initialization("Could not find Agent_OnLoad function in the agent library", agent->name());
    }
  }
  JvmtiExport::enter_primordial_phase();
}

//下边这小段代码在:/hotspot/src/share/vm/runtime/arguments.hpp  中
//说明:上边的 create_vm_init_agents方法中的  Arguments::agents()  ,
//其实就是从agent链表中取第一个,代码为:
static AgentLibrary* agents()             { return _agentList.first(); }

这个方法的主要作用就是:

  • 遍历我们刚刚在参数解析时根据-javaagent的值构建的agents链表
  • 依次调用lookup_agent_on_load函数来找动态链接文件(在识别到我们vm参数中的-javaagent时,最终找的动态链接文件就是 libinstrument.so 文件)
  • 在找到后保存到了一个entry结构中,之后来执行这个entry中的方法, 也即:动态链接libinstrument.so中的Agent_OnLoad 方法。

紧接着我们大概看下是怎么找的

通过 lookup_on_load 来查找libinstrument.so文件以及他的Agent_OnLoad方法

此片段的完整源码在 /hotspot/src/share/vm/runtime/thread.cpp 中

// Find the Agent_OnLoad entry point
static OnLoadEntry_t lookup_agent_on_load(AgentLibrary* agent) {
  const char *on_load_symbols[] = AGENT_ONLOAD_SYMBOLS;
  //调用lookup_on_load
  return lookup_on_load(agent, on_load_symbols, sizeof(on_load_symbols) / sizeof(char*));
}
// Find a command line agent library and return its entry point for
//         -agentlib:  -agentpath:   -Xrun
// num_symbol_entries must be passed-in since only the caller knows the number of symbols in the array.
static OnLoadEntry_t lookup_on_load(AgentLibrary* agent, const char *on_load_symbols[], size_t num_symbol_entries) {
  OnLoadEntry_t on_load_entry = NULL;
  void *library = NULL;

  if (!agent->valid()) {
    char buffer[JVM_MAXPATHLEN];
    char ebuf[1024];
    const char *name = agent->name();
    const char *msg = "Could not find agent library ";
    // First check to see if agent is statically linked into executable
    if (os::find_builtin_agent(agent, on_load_symbols, num_symbol_entries)) {
      library = agent->os_lib();
    } else if (agent->is_absolute_path()) {
      library = os::dll_load(name, ebuf, sizeof ebuf);
      if (library == NULL) {
        const char *sub_msg = " in absolute path, with error: ";
        size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
        char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
        jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
        // If we can't find the agent, exit.
        vm_exit_during_initialization(buf, NULL);
        FREE_C_HEAP_ARRAY(char, buf, mtThread);
      }
    } else {
      // Try to load the agent from the standard dll directory
      if (os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(),
                             name)) {
        library = os::dll_load(buffer, ebuf, sizeof ebuf);
      }
      if (library == NULL) { // Try the local directory
        char ns[1] = {0};
        if (os::dll_build_name(buffer, sizeof(buffer), ns, name)) {
          library = os::dll_load(buffer, ebuf, sizeof ebuf);
        }
        if (library == NULL) {
          const char *sub_msg = " on the library path, with error: ";
          size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
          char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
          jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
          // If we can't find the agent, exit.
          vm_exit_during_initialization(buf, NULL);
          FREE_C_HEAP_ARRAY(char, buf, mtThread);
        }
      }
    }
    agent->set_os_lib(library);
    agent->set_valid();
  }

  //Find the OnLoad function. 查询OnLoad方法 ,其实最终内部会在查询时将Agent加到前边,
  //也就是会变成这样: Agent_On(Un)Load/Attach<_lib_name>  了解即可
  on_load_entry =
    CAST_TO_FN_PTR(OnLoadEntry_t, os::find_agent_function(agent,
                                                          false,
                                                          on_load_symbols,
                                                          num_symbol_entries));
  return on_load_entry;
}

注意,因为本小节我们分析的是静态加载,所以只关注-javaagent这个逻辑,解析这个参数时 传入add_init_agent方法的第三个参数 是falseimage.png 而这个参数就是 AgentLibrary的 is_absolute_path,所以根据这里我们可以得出 当使用-javaagent这种方式静态加载Java Agent时 走的是lookup_on_load方法的 else逻辑 ,也就是在我们使用-javaagent加载agent.jar时 ,走的是这段代码:

else {
      // Try to load the agent from the standard dll directory
      if (os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(),
                             name)) {
        library = os::dll_load(buffer, ebuf, sizeof ebuf);
      }
      if (library == NULL) { // Try the local directory
        char ns[1] = {0};
        //构建将要加载的 动态链接文件的名称
        if (os::dll_build_name(buffer, sizeof(buffer), ns, name)) {
          //根据构建后的动态链接文件名称  加载(load)动态链接文件到内存
          library = os::dll_load(buffer, ebuf, sizeof ebuf);
        }
        if (library == NULL) {
          const char *sub_msg = " on the library path, with error: ";
          size_t len = strlen(msg) + strlen(name) + strlen(sub_msg) + strlen(ebuf) + 1;
          char *buf = NEW_C_HEAP_ARRAY(char, len, mtThread);
          jio_snprintf(buf, len, "%s%s%s%s", msg, name, sub_msg, ebuf);
          // If we can't find the agent, exit.
          vm_exit_during_initialization(buf, NULL);
          FREE_C_HEAP_ARRAY(char, buf, mtThread);
       }
    }
}

这段代码中先是根据name去构建了动态链接文件(win中是dll,linux下是.so) 的名称,这个其实就是为什么我们传入的是instrument 而真正执行的动态链接文件是 libinstrument.so的原因。如下是构建动态连接文件的代码截图: image.png

之后就是加载动态链接文件,然后就是寻找OnLoad也就是上边提到的find_agent_function ,最终会将找到的动态连接文件中的Agent_OnLoad方法保存到一个entry中并返回,之后就是执行动态链接库中的Agent_OnLoad方法了也即上边已经说过的代码: image.png 到此,寻找动态链接库以及执行动态链接库中的方法就分析完了

找到libinstrument.so的真正实现InvocationAdapter.c

而实际上 libinstrument.so 这个动态链接库的实现是位于java/instrumentat/share/native/libinstrument 入口的InvocationAdapter.c 我们不妨来简单看下: image.png

在上边的create_vm_init_agents函数中 我们查找并执行了动态链接库libinstrument.so中的Agnet_OnLoad函数,而这个函数最终会执行到InvocationAdapter.c的Agent_OnLoad中,下边是此方法的代码:

执行Agent_OnLoad函数

这个方法的注释很重要(见下边代码中的注释),这里简单翻译下

  1. 此方法将被命令行上的每一个 -javaagent 参数调用一次 (因为-javaagent后边可以加多个agent jar 也就是说有几个agent jar就执行此方法几次)。
  2. 每次调用将创建属于自己的agent和agent相关的数据
  3. 解析jar文件和后边的参数(我们要知道 -javaagent可以这么配:-javaagent:xxxagent.jar=option1=value1,option2=value2)
  4. 读取jar的配置文件MANIFEST里Premain-Class,并且把jar文件追加到agent的class path中。

代码位于: /jdk/src/share/instrument/InvocationAdapter.c 

/*
 *  This will be called once for every -javaagent on the command line.
 *  Each call to Agent_OnLoad will create its own agent and agent data.
 *
 *  The argument tail string provided to Agent_OnLoad will be of form
 *  <jarfile>[=<options>]. The tail string is split into the jarfile and
 *  options components. The jarfile manifest is parsed and the value of the
 *  Premain-Class attribute will become the agent's premain class. The jar
 *  file is then added to the system class path, and if the Boot-Class-Path
 *  attribute is present then all relative URLs in the value are processed
 *  to create boot class path segments to append to the boot class path.
 */
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) {
    JPLISInitializationError initerror  = JPLIS_INIT_ERROR_NONE;
    jint                     result     = JNI_OK;
    JPLISAgent *             agent      = NULL;
    
    //1. 创建 JPLISAgent 专门为java提供的 JVMTI agent(重要的一步)
    initerror = createNewJPLISAgent(vm, &agent);
    if ( initerror == JPLIS_INIT_ERROR_NONE ) {
        int             oldLen, newLen;
        char *          jarfile;
        char *          options;
        jarAttribute*   attributes;
        char *          premainClass;
        char *          agentClass;
        char *          bootClassPath;
        /*
         * Parse <jarfile>[=options] into jarfile and options,解析option也就是我们-javaagent:xxxagent.jar=option1=value1 中的 option1=value1参数
         */
      

        /*
         * Agent_OnLoad is specified to provide the agent options
         * argument tail in modified UTF8. However for 1.5.0 this is
         * actually in the platform encoding - see 5049313.
         *
         * Open zip/jar file and parse archive. If can't be opened or
         * not a zip file return error. Also if Premain-Class attribute
         * isn't present we return an error.
         */
        //读取jar文件中的一些信息
        attributes = readAttributes(jarfile);
        
        //2. 寻找 jar中MANIFEST.MF 中的 Premain-Class 类
        premainClass = getAttribute(attributes, "Premain-Class");
        
        //3. 把jar文件追加到agent的class path中。
        /*
         * Add to the jarfile 
         */
        appendClassPath(agent, jarfile);
        ...一些校验 这里我们略过 否则太占地
    }
    ....略
    return result;
}


创建与初始化 JPLISAgent

在createNewJPLISAgent中 创建了一个 JPLISAgent (Java Programming Language Instrumentation Services Agent),并且从Vm环境中获取了 jvmtiEnv 指针,用于后续的操作,jvmtiEnv是一个很重要的指针(在JVMTI运行时,通常一个JVMTI Agent对应一个jvmtiEnv)。

我们来看下 createNewJPLISAgent 的代码:

源码在:jdk8u/jdk/src/share/instrument/JPLISAgent.c


/*
 *  OnLoad processing code.
 */

/*
 *  Creates a new JPLISAgent.
 *  Returns error if the agent cannot be created and initialized.
 *  The JPLISAgent* pointed to by agent_ptr is set to the new broker,
 *  or NULL if an error has occurred.
 */
JPLISInitializationError
createNewJPLISAgent(JavaVM * vm, JPLISAgent **agent_ptr) {
    JPLISInitializationError initerror       = JPLIS_INIT_ERROR_NONE;
    jvmtiEnv *               jvmtienv        = NULL;
    jint                     jnierror        = JNI_OK;
    
    *agent_ptr = NULL;
    //获取jvmtienv指针从vm环境 ,jvmtienv 很重要 他是个指针,通过他可以和jvm交互
    jnierror = (*vm)->GetEnv(  vm,
                               (void **) &jvmtienv,
                               JVMTI_VERSION_1_1);
    if ( jnierror != JNI_OK ) {
        initerror = JPLIS_INIT_ERROR_CANNOT_CREATE_NATIVE_AGENT;
    } else {
        //分配空间
        JPLISAgent * agent = allocateJPLISAgent(jvmtienv);
        if ( agent == NULL ) {
            initerror = JPLIS_INIT_ERROR_ALLOCATION_FAILURE;
        } else {
            //初始化 JPLISAgent(很重要的一步)
            initerror = initializeJPLISAgent(  agent,
                                               vm,
                                               jvmtienv);
            if ( initerror == JPLIS_INIT_ERROR_NONE ) {
                *agent_ptr = agent;
            } else {
                deallocateJPLISAgent(jvmtienv, agent);
            }
        }
        //一些异常处理 略
    }
    return initerror;
}

其中我们比较关注的一步就是 初始化JPLISAgent

源码在:jdk8u/jdk/src/share/instrument/JPLISAgent.c

JPLISInitializationError
initializeJPLISAgent(   JPLISAgent *    agent,
                        JavaVM *        vm,
                        jvmtiEnv *      jvmtienv) {
    jvmtiError      jvmtierror = JVMTI_ERROR_NONE;
    jvmtiPhase      phase;

    agent->mJVM                                      = vm;
    agent->mNormalEnvironment.mJVMTIEnv              = jvmtienv;
    agent->mNormalEnvironment.mAgent                 = agent;
    agent->mNormalEnvironment.mIsRetransformer       = JNI_FALSE;
    agent->mRetransformEnvironment.mJVMTIEnv         = NULL;        /* NULL until needed */
    agent->mRetransformEnvironment.mAgent            = agent;
    agent->mRetransformEnvironment.mIsRetransformer  = JNI_FALSE;   /* JNI_FALSE until mJVMTIEnv is set */
    agent->mAgentmainCaller                          = NULL;
    agent->mInstrumentationImpl                      = NULL;
    agent->mPremainCaller                            = NULL;
    agent->mTransform                                = NULL;
    agent->mRedefineAvailable                        = JNI_FALSE;   /* assume no for now */
    agent->mRedefineAdded                            = JNI_FALSE;
    agent->mNativeMethodPrefixAvailable              = JNI_FALSE;   /* assume no for now */
    agent->mNativeMethodPrefixAdded                  = JNI_FALSE;
    agent->mAgentClassName                           = NULL;
    agent->mOptionsString                            = NULL;

    /* make sure we can recover either handle in either direction.
     * the agent has a ref to the jvmti; make it mutual
     */
    jvmtierror = (*jvmtienv)->SetEnvironmentLocalStorage(
                                            jvmtienv,
                                            &(agent->mNormalEnvironment));

    
    //1. 在此处监听VMInit事件!
    /* now turn on the VMInit event */
    if ( jvmtierror == JVMTI_ERROR_NONE ) {
        jvmtiEventCallbacks callbacks;
        memset(&callbacks, 0, sizeof(callbacks));
        //2. 在监听到VMinit 初始化事件后执行 eventHandlerVMInit方法的逻辑 (重要的一步)
        callbacks.VMInit = &eventHandlerVMInit;

        jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
                                                     &callbacks,
                                                     sizeof(callbacks));
        check_phase_ret_blob(jvmtierror, JPLIS_INIT_ERROR_FAILURE);
        jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
    }

    if ( jvmtierror == JVMTI_ERROR_NONE ) {
        jvmtierror = (*jvmtienv)->SetEventNotificationMode(
                                                jvmtienv,
                                                JVMTI_ENABLE,
                                                JVMTI_EVENT_VM_INIT,
                                                NULL /* all threads */);
        check_phase_ret_blob(jvmtierror, JPLIS_INIT_ERROR_FAILURE);
        jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
    }

    return (jvmtierror == JVMTI_ERROR_NONE)? JPLIS_INIT_ERROR_NONE : JPLIS_INIT_ERROR_FAILURE;
}

初始化JPLISAgent 做了两件我们比较关注的事情,就是:

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

image.png image.png image.png

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

执行eventHandlerVMInit方法

eventHandlerVMInit方法比较重要,紧接着我们来看下:

源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c

/*
 *  JVMTI callback support
 *
 *  We have two "stages" of callback support.
 *  At OnLoad time, we install a VMInit handler.
 *  When the VMInit handler runs, we remove the VMInit handler and install a
 *  ClassFileLoadHook handler.
 */

void JNICALL
eventHandlerVMInit( jvmtiEnv *      jvmtienv,
                    JNIEnv *        jnienv,
                    jthread         thread) {
    JPLISEnvironment * environment  = NULL;
    jboolean           success      = JNI_FALSE;
    // 从jvmtienv 中获取JPLISAgent的环境
    environment = getJPLISEnvironment(jvmtienv);

    /* process the premain calls on the all the JPL agents */
    if ( environment != NULL ) {
        jthrowable outstandingException = preserveThrowable(jnienv);
        //执行processJavaStart 开始
        success = processJavaStart( environment->mAgent,
                                    jnienv);
        restoreThrowable(jnienv, outstandingException);
    }

    /* if we fail to start cleanly, bring down the JVM */
    if ( !success ) {
        abortJVM(jnienv, JPLIS_ERRORMESSAGE_CANNOTSTART);
    }
}

执行processJavaStart函数

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

源码在:/jdk8u/jdk/src/share/instrument/JPLISAgent.c


/*
 *  VMInit processing code.
 */


/*
 * If this call fails, the JVM launch will ultimately be aborted,
 * so we don't have to be super-careful to clean up in partial failure
 * cases.
 */
jboolean
processJavaStart(   JPLISAgent *    agent,
                    JNIEnv *        jnienv) {
    jboolean    result;

    /*
     *  OK, Java is up now. We can start everything that needs Java.
     * ok , Java 现在已经启动了。我们可以开始运行所有需要 Java 的应用程序了。
     */
     
    /*
     *  First make our emergency fallback InternalError throwable.
     */
    result = initializeFallbackError(jnienv);
    jplis_assert(result);

    /*
     *  Now make the InstrumentationImpl instance.
     *
     * 现在创建 InstrumentationImpl的实例,在这里我们知道:
     * InstrumentationImpl的实例不是在Java中new 的,而是由jvm创建的,通过premain方法传给java然后就可以使用了。
     */
    if ( result ) {
        result = createInstrumentationImpl(jnienv, agent);
        jplis_assert(result);
    }
    /*
     *  Then turn off the VMInit handler and turn on the ClassFileLoadHook.
     *  This way it is on before anyone registers a transformer.
     *
     * 在此方法中注册类加载时的回调函数 (ClassFileLoadHook),
     * 对应的最终实现就是 ClassFileTransformer的 transform
     */
    if ( result ) {
        result = setLivePhaseEventHandlers(agent);
        jplis_assert(result);
    }
    /*
     *  Load the Java agent, and call the premain.
     * 加载java agent并调用premain方法,看到没这就是调用premain方法的地方!
     */
    if ( result ) {
        result = startJavaAgent(agent, jnienv,
                                agent->mAgentClassName, agent->mOptionsString,
                                agent->mPremainCaller);
    }
    /*
     * Finally surrender all of the tracking data that we don't need any more.
     * If something is wrong, skip it, we will be aborting the JVM anyway.
     */
    if ( result ) {
        deallocateCommandLineData(agent);
    }
    return result;
}

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

  1. 创建 (sun.instrument.InstrumentationImpl)类的实例
  2. 监听&开启 ClassFileLoadHook 事件,注册回调函数最终此回调函数会调用到:ClassFileTransformer的 transform 。
  3. 加载java agent并调用premain方法(会把Instrumentation类实例和agent参数传入premain方法中去),premain中会将ClassFileTransformer的的实现添加进 Instrumentation类的实例中去
开启并监听ClassFileLoadHook事件 -> setLivePhaseEventHandlers

而其中的第二步即 :监听&开启 ClassFileLoadHook 事件,里边的操作比较重要我们要知道,所以下边看下源码:

源码在:/jdk8u/jdk/src/share/instrument/JPLISAgent.c

jboolean
setLivePhaseEventHandlers(  JPLISAgent * agent) {
    jvmtiEventCallbacks callbacks;
    jvmtiEnv *          jvmtienv = jvmti(agent);
    jvmtiError          jvmtierror;

    /* first swap out the handlers (switch from the VMInit handler, which we do not need,
     * to the ClassFileLoadHook handler, which is what the agents need from now on)
     */
    memset(&callbacks, 0, sizeof(callbacks));
    //设置回调事件处理器
    callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;

    jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
                                                 &callbacks,
                                                 sizeof(callbacks));
    check_phase_ret_false(jvmtierror);
    jplis_assert(jvmtierror == JVMTI_ERROR_NONE);


    if ( jvmtierror == JVMTI_ERROR_NONE ) {
        /* turn off VMInit */
        jvmtierror = (*jvmtienv)->SetEventNotificationMode(
                                                    jvmtienv,
                                                    JVMTI_DISABLE,
                                                    JVMTI_EVENT_VM_INIT,
                                                    NULL /* all threads */);
        check_phase_ret_false(jvmtierror);
        jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
    }

    if ( jvmtierror == JVMTI_ERROR_NONE ) {
        /* turn on ClassFileLoadHook */
        //启用ClassFileLoadHook事件
        jvmtierror = (*jvmtienv)->SetEventNotificationMode(
                                                    jvmtienv,
                                                    JVMTI_ENABLE,
                                                    JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
                                                    NULL /* all threads */);
        check_phase_ret_false(jvmtierror);
        jplis_assert(jvmtierror == JVMTI_ERROR_NONE);
    }

    return (jvmtierror == JVMTI_ERROR_NONE);
}

上边这个函数中会设置 ClassFileLoadHook 的处理器,即类加载时的回调处理器 eventHandlerClassFileLoadHook

但是有一点我们要清楚,这里只是设置回调函数,并没有真正执行eventHandlerClassFileLoadHook的内容,因为此时还不到类加载阶段,切记这一点

在这个eventHandlerClassFileLoadHook里边会最终调用(注意不是此时调用,而是类加载时)到我们的 jdk中的ClassFileTransformer接口的transform方法,接下来我们看下:

设置类加载时的回调函数处理器:eventHandlerClassFileLoadHook

源码在:/jdk8u/jdk/src/share/instrument/InvocationAdapter.c


void JNICALL
eventHandlerClassFileLoadHook(  jvmtiEnv *              jvmtienv,
                                JNIEnv *                jnienv,
                                jclass                  class_being_redefined,
                                jobject                 loader,
                                const char*             name,
                                jobject                 protectionDomain,
                                jint                    class_data_len,
                                const unsigned char*    class_data,
                                jint*                   new_class_data_len,
                                unsigned char**         new_class_data) {
    JPLISEnvironment * environment  = NULL;

    environment = getJPLISEnvironment(jvmtienv);

    /* if something is internally inconsistent (no agent), just silently return without touching the buffer */
    if ( environment != NULL ) {
        jthrowable outstandingException = preserveThrowable(jnienv);
        transformClassFile( environment->mAgent,
                            jnienv,
                            loader,
                            name,
                            class_being_redefined,
                            protectionDomain,
                            class_data_len,
                            class_data,
                            new_class_data_len,
                            new_class_data,
                            environment->mIsRetransformer);
        restoreThrowable(jnienv, outstandingException);
    }
}

上边这个 eventHandlerClassFileLoadHook方法就是监听到类加载时的处理逻辑。其中的transformClassFile 会执行到我们的java代码,见下边:

调用到java代码的地方 -> transformClassFile

源码在: /jdk8u/jdk/src/share/instrument/JPLISAgent.c

/*
 *  Support for the JVMTI callbacks
 */

void
transformClassFile(             JPLISAgent *            agent,
                                JNIEnv *                jnienv,
                                jobject                 loaderObject,
                                const char*             name,
                                jclass                  classBeingRedefined,
                                jobject                 protectionDomain,
                                jint                    class_data_len,
                                const unsigned char*    class_data,
                                jint*                   new_class_data_len,
                                unsigned char**         new_class_data,
                                jboolean                is_retransformer) {
    jboolean        errorOutstanding        = JNI_FALSE;
    jstring         classNameStringObject   = NULL;
    jarray          classFileBufferObject   = NULL;
    jarray          transformedBufferObject = NULL;
    jsize           transformedBufferSize   = 0;
    unsigned char * resultBuffer            = NULL;
    jboolean        shouldRun               = JNI_FALSE;
        
        //.........略过n多行代码.........
        
        /*  now call the JPL agents to do the transforming */
        /*  potential future optimization: may want to skip this if there are none */
        //!!!!!!!  这一步相当重要,他就是调用到我们java代码(`InstrumentationImpl`类的`transform`方法)
        //的地方
        if ( !errorOutstanding ) {
            jplis_assert(agent->mInstrumentationImpl != NULL);
            jplis_assert(agent->mTransform != NULL);
            //调用jdk中InstrumentationImpl类的的transform方法
            transformedBufferObject = (*jnienv)->CallObjectMethod(
                                                jnienv,
                                                agent->mInstrumentationImpl,
                                                agent->mTransform,
                                                loaderObject,
                                                classNameStringObject,
                                                classBeingRedefined,
                                                protectionDomain,
                                                classFileBufferObject,
                                                is_retransformer);
            errorOutstanding = checkForAndClearThrowable(jnienv);
            jplis_assert_msg(!errorOutstanding, "transform method call failed");
        }
        //.........略过n多行代码.........
    }
    return;
}
找到将被调用(注意不是此时调用)的java代码!!!(InstrumentationImpl类的transform方法)

而上边这个(transformedBufferObject = (*jnienv)->CallObjectMethod(n多个参数))这段代码最终就会调到jdk中InstrumentationImpl类的的transform方法,如下: image.png image.png image.png 我去,终于看到自己写的代码了,不容易啊。翻山越岭的。

在开启监听类加载事件 并 注册完类加载时的回调函数后,进行下边逻辑

加载java agent并调用premain方法——> startJavaAgent

image.png image.png 调用我们MAINFEST.MF Premain-Class类中的premain方法并传入参数(包括启动时-javaagent:xxjava.jar=option1=value1=option2=value2传入的参数和Instrumentation的实例对象) image.png

调用到jdk代码-> sun.instrument.InstrumentationImpl的loadClassAndCallPremain

注意:loadClassAndCallPremain中会调用loadClassAndStartAgent方法

java代码如下:

代码在 sun.instrument.InstrumentationImpl类中

/**
* 静态加载时 被jvm直接调用的是loadClassAndCallPremain这个方法
*
*/

// WARNING: the native code knows the name & signature of this method
private void
loadClassAndCallPremain(    String  classname,
                            String  optionsString)
        throws Throwable {
    //静态加载调用的最终方法名: premain
    loadClassAndStartAgent( classname, "premain", optionsString );
}


// Attempt to load and start an agent
//从这里启动并加载一个agent
private void
loadClassAndStartAgent( String  classname,
                        String  methodname,
                        String  optionsString)
        throws Throwable {

    ClassLoader mainAppLoader   = ClassLoader.getSystemClassLoader();
    Class<?>    javaAgentClass  = mainAppLoader.loadClass(classname);

    Method m = null;
    NoSuchMethodException firstExc = null;
    boolean twoArgAgent = false;

    // The agent class must have a premain or agentmain method that
    // has 1 or 2 arguments. We check in the following order:
    //
    // 1) declared with a signature of (String, Instrumentation)
    // 2) declared with a signature of (String)
    // 3) inherited with a signature of (String, Instrumentation)
    // 4) inherited with a signature of (String)
    //
    // So the declared version of either 1-arg or 2-arg always takes
    // primary precedence over an inherited version. After that, the
    // 2-arg version takes precedence over the 1-arg version.
    //
    // If no method is found then we throw the NoSuchMethodException
    // from the first attempt so that the exception text indicates
    // the lookup failed for the 2-arg method (same as JDK5.0).

    try {
        m = javaAgentClass.getDeclaredMethod( methodname,
                             new Class<?>[] {
                                 String.class,
                                 java.lang.instrument.Instrumentation.class
                             }
                           );
        twoArgAgent = true;
    } catch (NoSuchMethodException x) {
        // remember the NoSuchMethodException
        firstExc = x;
    }

    if (m == null) {
        // now try the declared 1-arg method
        try {
            m = javaAgentClass.getDeclaredMethod(methodname,
                                             new Class<?>[] { String.class });
        } catch (NoSuchMethodException x) {
            // ignore this exception because we'll try
            // two arg inheritance next
        }
    }

    if (m == null) {
        // now try the inherited 2-arg method
        try {
            m = javaAgentClass.getMethod( methodname,
                             new Class<?>[] {
                                 String.class,
                                 java.lang.instrument.Instrumentation.class
                             }
                           );
            twoArgAgent = true;
        } catch (NoSuchMethodException x) {
            // ignore this exception because we'll try
            // one arg inheritance next
        }
    }

    if (m == null) {
        // finally try the inherited 1-arg method
        try {
            m = javaAgentClass.getMethod(methodname,
                                         new Class<?>[] { String.class });
        } catch (NoSuchMethodException x) {
            // none of the methods exists so we throw the
            // first NoSuchMethodException as per 5.0
            throw firstExc;
        }
    }

    // the premain method should not be required to be public,
    // make it accessible so we can call it
    // Note: The spec says the following:
    //     The agent class must implement a public static premain method...
    setAccessible(m, true);

    //通过反射执行传入的方法名称premian(静态加载时传的是premain,动态加载传的是agentmain),
    //即:我们在MAINFEST.MF中指定的Premain-Class类里边的premain方法
    
    // invoke the 1 or 2-arg method
    if (twoArgAgent) {
        m.invoke(null, new Object[] { optionsString, this });
    } else {
        m.invoke(null, new Object[] { optionsString });
    }
}
调用到我们MAINFEST.MF文件中-> Premain-Class类中的premain方法(我们自己开发的代码)

loadClassAndStartAgent最终会通过反射执行我们在MAINFEST.MF中指定的Premain-Class类里边的premain方法,值的注意的是:在premain方法中其实只是往 InstrumentationImpl 实例中添加了我们自己定义的类转换器(比如我的DefineTransformer类),还没有真正的执行DefineTransformertransform函数

以下是我的premain方法: image.png

那么什么时候会执行(或者说 回调,这个词更符合此函数的调用动作)到我的DefineTransformer类中的tranform方法去修改(Retransform) 或者 重新定义(Redefine) 类呢?那肯定是类加载时啊,上边我们说过很多遍了!

加载类的入口: systemDictionary.cpp-> load_instance_class

而jvm中类加载是从这个地方开始的(systemDictionary.cpp): image.png 因为我们自己编写的类都是要通过系统类加载器加载的,所以会走到这个系统类加载,我们继续跟,来到classLoader.cpp中的 load_calassfile方法,如下: image.png

类加载时回调在premain中设置的转换器,此处的话就是: DefineTransformer类的transform方法

注意我们本文中的 DefineTransformer 类实现了 java.lang.instrument.ClassFileTransformer接口的transform方法!所以才会调用到DefineTransformer类的transform方法!这一点要明白!

继续跟进load_calassfile中的 parseClassFile方法: ps: 这个方法巨长,至少有600多行,类加载的主要逻辑就在这里边了,感兴趣可以去看看完整的,这里我们不粘完整版本了,只保留我们感兴趣的,调用类加载时候的钩子函数片段,代码如下:

源码在: hotspot/src/share/vm/classfile/classFileParser.cpp 中

instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name,
                                                    ClassLoaderData* loader_data,
                                                    Handle protection_domain,
                                                    KlassHandle host_klass,
                                                    GrowableArray<Handle>* cp_patches,
                                                    TempNewSymbol& parsed_name,
                                                    bool verify,
                                                    TRAPS) {

  // When a retransformable agent is attached, JVMTI caches the
  // class bytes that existed before the first retransformation.
  // If RedefineClasses() was used before the retransformable
  // agent attached, then the cached class bytes may not be the
  // original class bytes.
  JvmtiCachedClassFileData *cached_class_file = NULL;
  Handle class_loader(THREAD, loader_data->class_loader());
  bool has_default_methods = false;
  bool declares_default_methods = false;
  // JDK-8252904:
  // The stream (resource) attached to the instance klass may
  // be reallocated by this method. When JFR is included the
  // stream may need to survive beyond the end of the call. So,
  // the caller is expected to declare the ResourceMark that
  // determines the lifetime of resources allocated under this
  // call.

  ClassFileStream* cfs = stream();
  // Timing
  assert(THREAD->is_Java_thread(), "must be a JavaThread");
  JavaThread* jt = (JavaThread*) THREAD;

  init_parsed_class_attributes(loader_data);

  if (JvmtiExport::should_post_class_file_load_hook()) {
    
    JvmtiThreadState *state = jt->jvmti_thread_state();
    if (state != NULL) {
      KlassHandle *h_class_being_redefined =
                     state->get_class_being_redefined();
      if (h_class_being_redefined != NULL) {
        instanceKlassHandle ikh_class_being_redefined =
          instanceKlassHandle(THREAD, (*h_class_being_redefined)());
        cached_class_file = ikh_class_being_redefined->get_cached_class_file();
      }
    }

    unsigned char* ptr = cfs->buffer();
    unsigned char* end_ptr = cfs->buffer() + cfs->length();
    //在此处回调我们设置的 回调函数,本文是: DefineTransformer类的transform函数
    JvmtiExport::post_class_file_load_hook(name, class_loader(), protection_domain,
                                           &ptr, &end_ptr, &cached_class_file);

    if (ptr != cfs->buffer()) {
      // JVMTI agent has modified class file data.
      // Set new class file stream using JVMTI agent modified
      // class file data.
      cfs = new ClassFileStream(ptr, end_ptr - ptr, cfs->source());
      set_stream(cfs);
    }
  }
  //...........  此处略去至少 400 ~  500 行代码 ,想目睹类加载详情的,建议看看。很精彩  ...........
  
  // Clear class if no error has occurred so destructor doesn't deallocate it
  _klass = NULL;
  return this_klass;
}

jvmtiExport.cpp -> post_class_file_load_hook: image.png jvmtiExport.cpp -> post_all_envs: image.png jvmtiExport.cpp -> post_all_envs中的 post_to_env: image.png 上边方法post_to_env中的这段:

jvmtiEventClassFileLoadHook callback = env->callbacks()->ClassFileLoadHook;
    if (callback != NULL) {
      (*callback)(env->jvmti_external(), jni_env,
                  jem.class_being_redefined(),
                  jem.jloader(), jem.class_name(),
                  jem.protection_domain(),
                  _curr_len, _curr_data,
                  &new_len, &new_data);
    }

首先会直接调用InstrumentationImpl中的transform,之后此方法会间接调用到我们编写的DefineTransformer(实现了ClassFileTransformer接口的transform)类的transform方法!!! 我的类 增强or修改 方法如下: image.png

将修改后的字节码保存到类文件流中去

在调用完DefineTransformer类的transform方法后,从上边可以看到返回了修改后的字节码,需要将修改后的类数据添加到类文件流,使得修改后的内容生效呀(最终加载到元空间的是我们在DefineTransformer类transform方法 修改后的内容),所以就有了下边的代码: image.png

执行加载后边的逻辑: -> 链接(验证,准备,解析)-> 初始化 -> 使用(如new or 反射 等等)

在 初始化这一步之后,类的元数据被保存到了元空间(1.8后引入的)中,之后我们就可以愉快的使用了,比如new 或者反射等等根据类元数据创建实例这类行为,或者访问类的元数据比如 类.class 等等操作。


到此,就算真正的将静态加载jar以及插桩是如何执行的这些流程串联起来了。真不容易。我都不知道我怎么坚持下来的😄 整个流程比较复杂,观看代码太枯燥,还是画个图吧,更直观(一图胜千言!) 如下:

静态加载图解(重要,重要,重要!)

image.png 上图简单语言概括下:

  1. 【通过main函数启动java程序】
  2. 【cerate_vm开始】
    • 2.1、注册 虚拟机初始化时 (对应事件类型是 VMInit) 事件发生时的回调函数:eventHandlerVMinit
    • 2.2、vm初始化开始,回调步骤2.1 设置的回调函数:eventHandlerVMinit
      • 2.2.1、注册 类加载时(对应事件类型是 ClassFileLoadHook)的回调函数为 InstrumentationImpl的transform (最终的实现是ClassFileTransformer接口的实现类里的transform方法)

      • 2.2.2、直接执行的是InstrumentationImpl的loadClassAndStartAgent方法,最终调用到agent中的Premain-Class中的premain方法(该方法是往Instrumentation实例中设置了类转换器,并没有真正执行类转换的操作

  3. 【create_vm函数执行完毕,开始类加载工作】
    • 3.1、加载
      • 3.1.1、回调步骤2.2.1 中设置的类加载时的回调函数:InstrumentationImpl的transform(最终会调用到实现了ClassFileTransformer接口的实现类里的transform方法),进行类的增强 or 修改等操作,并返回修改后的字节码。

      • 3.1.2、将修改后的字节码生效。即保存到类数据文件流中。

    • 3.2、后续操作: -> 链接(验证、准备、解析)-> 初始化-> 使用

到此,你清楚静态加载时Java Agent的工作原理和机制了吗???ok接下来我们说说动态加载。

4、Java Agent 动态加载演示、图解、源码分析

动态加载相较于静态加载,会更灵活一点,我们演示下如何实现一个动态加载的agent。

动态加载demo实现与演示

想要达到的效果(让Integer.valueOf(int i)每次都装箱,不从IntegerCache数组中取,也就是要达到-127-128两个Integer对象之间的对比也会返回false)

我们知道如果你声明了两个局部变量:( Integer i1=20;Integer i2=20;),编译为class后将会被Integer.valueOf(int i);方法包装(可以参考我之前的文章:Integer缓存那点小事儿),去==比较时会返回true,这个原因是因为当i 在-128-127范围内时,valueOf不会将i装箱,而是从缓存数组中取对应索引的Integer对象,相同的值取得是相同索引位置的对象 ==比较时自然是相等,而我们此处的案例想要的目的是:i1和i2去==比较时是不相等的,想要达到这个目的就得修改Integer.valueOf(int i);方法的实现,将-128-127的int值都装箱,这样的话 只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象

修改的 Integer.value(int i); 代码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

修改的 Integer.value(int i); 代码:

public static Integer valueOf(int i) {
    
    return new Integer(i);
}

修改jdk代码肯定是行不通,人家也不让你直接修改,我们这里准备通过agent修改,然后动态加载agent jar ,是不是很熟悉?没错 热部署 就是类似的原理。即不用重启即让代码生效。

编写agent jar的逻辑实现

基本上编写一个agent jar需要三个内容

编写agentmain方法(即加载agent的入口)

image.png

编写transform类转换方法(即对类/方法/字段进行字节码修改的地方)

截图放不下,直接贴代码:



/**
 * @Author: 黄壮壮
 * @Date: 2024/4/23 10:37:11
 * @Description:
 */
public class AttachAgent {

    static class ByAttachLoadAgentTransformer implements ClassFileTransformer {
       @Override
       public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
          //操作Integer类
          if ("java/lang/Integer".equals(className)) {
             CtClass clazz = null;
             System.out.println("动态加载agent jar -> 动态插桩Integer.valueOf方法【开始】," + "当前类:" + className);
             try {
                // 从ClassPool获得CtClass对象 (ClassPool对象是CtClass对象的容器,CtClass对象是类文件的抽象表示)
                final ClassPool classPool = ClassPool.getDefault();
                //不配这个 将找不到 java.lang.Integer 类
                classPool.insertClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
                clazz = classPool.get("java.lang.Integer");
                //获取到Integer的valueOf(int i) 方法。注意此处需要指定形参是int的 因为有多个valueOf方法
                CtMethod valueOf = clazz.getDeclaredMethod("valueOf", new CtClass[]{CtClass.intType});
                //(修改字节码) 这里对 java.lang.Integer的valueOf(int i)进行改写,将以下代码:
                /**
                 *     public static Integer valueOf(int i) {
                 *         if (i >= IntegerCache.low && i <= IntegerCache.high)
                 *             return IntegerCache.cache[i + (-IntegerCache.low)];
                 *         return new Integer(i);
                 *     }
                 *
                 *     改为:
                 *     public static Integer valueOf(int i) {
                 *         System.out.println("修改valueOf方法的实现,将-128-127的int值都装箱,这样的话 只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象");
                 *         return new Integer(i);
                 *     }
                 */

                //在此处修改valueOf方法的实现,将-128-127的int值都装箱,这样的话只要被valueOf包装过。那么去比较时就都是 false 了,因为是不同的对象
                String methodBody = "{" +
                                  "return new Integer($1);" +
                               "}";
                valueOf.setBody(methodBody);
                //通过CtClass的toBytecode(); 方法来获取 被修改后的字节码
                return clazz.toBytecode();
             } catch (Exception ex) {
                ex.printStackTrace();
             } finally {
                if (null != clazz) {
                   //调用CtClass对象的detach()方法后,对应class的其他方法将不能被调用。但是,你能够通过ClassPool的get()方法,
                   //重新创建一个代表对应类的CtClass对象。如果调用ClassPool的get()方法, ClassPool将重新读取一个类文件,并且重新创建一个CtClass对象,并通过get()方法返回
                   //如下所说:
                   //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载
                   clazz.detach();
                }
                System.out.println("动态加载agent jar -> 动态插桩Integer.valueOf方法【结束】," + "当前类:" + className);
             }
          }
          return classfileBuffer;
       }
    }

}

编写maven的一些属性,让其生成MAINFEST.MF文件(用到哪些就开启哪些,不用的最好关掉)

image.png

打jar包 并检查 META-INF/MANIFEST.MF中的内容

打包并查看xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies/META-INF/MANIFEST.MF中的内容,如下: image.png

编写目标程序,即:(将要被attach的java程序)

image.png

编写发起attach的程序 即:(请求jvm 动态加载agent jar)


/**
 * @Author: 黄壮壮
 * @Date: 2023/3/3 09:15:21
 * @Description:
 */
public class AttachAgentTest {

   public static void main(String[] args)throws Exception {
      //1. 根据进程id 与目标jvm程序建立 socket连接
      VirtualMachine vm = VirtualMachine.attach("目标程序的pid");
      try {
         //2. 加载指定的 agent jar,本质是发送请求
         vm.loadAgent("/usr/local/src/agent/attach/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
//       vm.loadAgent("/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");
      } finally {
         //程序结束时 卸载agent jar
//       vm.detach();
      }
      Thread.sleep(20000);
   }
}

演示效果

接下来将这三个文件上传到我的linux服务器:

image.png

javac 编译并运行 AttachTarget

image.png

将AttachTarget的pid输入到attach发起程序AttachAgentTest中,编译并运行 AttachAgnetTest

image.png image.png image.png

最终动态加载agent jar的效果:

image.png 可以看到,通过动态加载成功插桩到java.lang.Integer的valueOf方法实现了运行时修改类无需重启的效果。此处只是个小案例,如果配合ASM(更强大功能齐全的字节码操作工具)或者其他技术, 可能还会发挥出更强大的功能,但是要走正路,不能搞破坏,哈哈~。

动态加载源码解析

上边简单演示了下动态加载的使用,下边我们还是称热打铁从源码角度分析

发起attach的源码分析,这要从java代码-> 【VirtualMachine.attach("目标程序的pid")】开始看起:

com.sun.tools.attach.VirtualMachine类的attach方法:

public static VirtualMachine attach(String id)
    throws AttachNotSupportedException, IOException
{
    if (id == null) {
        throw new NullPointerException("id cannot be null");
    }
    List<AttachProvider> providers = AttachProvider.providers();
    if (providers.size() == 0) {
        throw new AttachNotSupportedException("no providers installed");
    }
    AttachNotSupportedException lastExc = null;
    for (AttachProvider provider: providers) {
        try {
            //开始attach
            return provider.attachVirtualMachine(id);
        } catch (AttachNotSupportedException x) {
            lastExc = x;
        }
    }
    throw lastExc;
}

之后会进入 provider.attachVirtualMachine(id); 这个逻辑里边,

而这个AttachProvider是一个抽象类,想要实现attach必须间接实现此类的attachVirtualMachine 方法(需要直接实现HotSpotVirtualMachine类),不同系统有不同的实现,如下: image.png 其实最好是以linux为例(因为我们的demo就是在linux系统上演示的),源码: image.png 但是因为我安装的是mac版本的jdk,所以我的java源码中的实现只有bsd系统(mac底层是基于bsd)的实现,即: BsdAttachProvider ,所以这里我们以bsd系统的 BsdAttachProvider 为例(值的注意的是BsdAttachProvider 和 LinuxAttachProvider 基本上一致 所以这里也就不纠结非得看LinuxAttachProvider的源码了): image.png

sun.tools.attach.BsdAttachProviderattachVirtualMachine方法如下:

public VirtualMachine attachVirtualMachine(String var1) throws AttachNotSupportedException, IOException {
    this.checkAttachPermission();
    this.testAttachable(var1);
    return new BsdVirtualMachine(this, var1);
}

类: sun.tools.attach.BsdVirtualMachine构造方法


BsdVirtualMachine(AttachProvider var1, String var2) throws AttachNotSupportedException, IOException {
    super(var1, var2);

    int var3;
    try {
        var3 = Integer.parseInt(var2);
    } catch (NumberFormatException var22) {
        throw new AttachNotSupportedException("Invalid process identifier");
    }
    //查找socket描述符 即/tmp/.java_pid+pid文件
    this.path = this.findSocketFile(var3);
    ///tmp/.java_pid+pid 文件为空的话 代表目标attach程序还没创建过 tmp/.java_pid+pid文件
    //也就是没启动和初始化 Attach listener线程
    if (this.path == null) {
        //创建/tmp//attach_pid+pid文件在宿主机,这个文件是 目标attach进程 判断收到的SIGQUIT信号是
        //dump 线程堆栈还是 attach请求的关键。
        File var4 = new File(tmpdir, ".attach_pid" + var3);
        createAttachFile(var4.getPath());

        try {
            //向目标程序发送 SIGQUIT 信号。(此时/tmp//attach_pid+pid文件已经被创建,
            //Signal Dispatch 线程(此线程在create_vm被创建启动后一直轮询等待信号产生)
            //在收到SIGQUIT信号后,将检测/tmp//attach_pid+pid文件 存在就执行
            //attach逻辑即启动Attach Listener 线程完成 socket bind listen准备接收数据,
            //不存在则进行线程堆栈dump 操作)
            sendQuitTo(var3);
            int var5 = 0;
            long var6 = 200L;
            int var8 = (int)(this.attachTimeout() / var6);//this.attachTimeout()默认值是10000
            
            //等待10秒 期间找到了tmp/.java_pid+pid文件(找到了的话代表Attach Listener线程
            //被创建成功可以开始进行attach了) 则往下走,10秒后没找到的话 抛出 
            //AttachNotSupportedException异常
            do {
                try {
                    Thread.sleep(var6);
                } catch (InterruptedException var21) {
                }
                this.path = this.findSocketFile(var3);
                ++var5;
            } while(var5 <= var8 && this.path == null);
            if (this.path == null) {
                throw new AttachNotSupportedException("Unable to open socket file: target process not responding or HotSpot VM not loaded");
            }
        } finally {
            var4.delete();
        }
    }
    //权限校验
    checkPermissions(this.path);

    //基于 tmp/.java_pid+pid 文件创建unix 套接字并连接
    int var24 = socket();

    try {
        //基于此unix 套接字,进行连接 ,之后就可以进程间通信了
        //(注意unix套接字,只能同机器不同进程间通信,而不能实现 不同机器间的通信!!!这一点一定要清楚)
        connect(var24, this.path);
    } finally {
        close(var24);
    }

}

可以看到最终BsdVirtualMachine构造方法中的逻辑是

  1. 查找/tmp目录下是否存在".java_pid"+pid文件(此文件就是unix套接字对应的文件,是unix套接字通信的基础) 如果不存在,则创建tmp/.attach_pid + pid文件(此文件是判断是否是attach的依据,如果找不到这个文件,则进行线程dump了,这个逻辑一会再源码可以看到),这个路径的来源可以参考下边的源码截图:

    • image.png image.png
  2. 然后发送 SIGQUIT 信号给目标进程,(在 sendQuitTo(var3);这行代码)

    • image.png
    • 最终调到/Users/hzz/myself_project/jdk_source_code/jdk8/jdk8u/jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c 这个jvm方法里边: image.png
      • 信号这个东西展开的话比较复杂我们简单描述下: 信号是某事件发生时对进程的通知机制,也被称为“软件中断”。信号可以看做是一种非常轻量级的进程间通信,信号由一个进程发送给另外一个进程,只不过是经由内核作为一个中间人发出,信号最初的目的是用来指定杀死进程的不同方式。 每个信号都有一个名字,以 “SIG” 开头,最熟知的信号应该是 SIGKILL 因为kill -9 pid 是非常常用的一个命令,而这个9其实就是信号SIGKILL的编号,kill -9 pid命令本质就是发送一个SIGKILL 信号给目标进程,目标进程收到命令后,强制退出。每个信号都有一个唯一的数字标识,从 1 开始。
        • 下面是常见的标准信号: image.png 因为上边在atach过程中sendQuitTo最终发送的是SIGQUIT信号,所以这里我们重点关注这个,像上边标准信号介绍那样,在linux系统中SIGQUIT信号一般用于退出系统,但是 在 Java 虚拟机(JVM)中,SIGQUIT 信号默认被处理为线程转储操作也就是说 thread dump。当 JVM 接收到 SIGQUIT 信号时,它通常会打印所有 Java 线程的当前堆栈跟踪到标准错误流(stderr)或指定的日志文件,而不是终止进程。这使得 SIGQUIT 成为在运行时调试和诊断 Java 应用程序时一个非常有用的工具,如下演示(使用kill -3 pid 发起SIGQUIT信号,目标进程控制台将输出 线程堆栈信息): image.png 其实jmap jstack这些工具就是通过SIGQUIT来实现的堆栈信息的输出的。but但是 在jvm中 接收到此信号时 并不只是用于堆栈信息的输出(因为我们上边说的是默认,而不是 只是dump线程),还有另一个处理逻辑就是响应处理attach请求(这个一会从源码【jdk8u/hotspot/src/share/vm/runtime/os.cppsignal_thread_entry方法中】可以看到)。
  3. 在向目标进程发送 SIGQUIT 信号后,attach发起端会进入一个do while循环

    • 如果在10秒后 (目标jvm还没创建Attach Listener线程, Attach Listener线程会创建"/tmp/.java_pid"+pid文件),那么将会抛出异常 AttachNotSupportedException("Unable to open socket file: target process not responding or HotSpot VM not loaded")
    • 此循环的逻辑如下图标红处描述: image.png如果找到了"/tmp/.java_pid"+pid文件 ,将会进行下边的逻辑
  4. 步骤3找到".java_pid"+pid文件后,attach 发起端将会基于此文件建立unix套接字(socket() )以及进行连接(connect() ),如下:

    • image.png
    • Unix 套接字 这个东西比较底层,更加深入的话可以参见书籍《UNIX网络编程卷一》(也称为 UNIX 域套接字,Unix Domain Sockets)是一种在同一台机器上不同进程之间进行数据交换通信机制。它是一种IPC(进程间通信)方法,提供了比其他通信方式(如管道和信号)更复杂的通信能力,关于这个知识点 更深入的我们不再展开,总之我们知道通过他 可以实现同一机器上不同进程之间的通信。值得注意的是 他和普通我们说的tcp udp可不一样,不具有不同机器之间的通信能力。另外他的接口和tcp udp非常像 也有 socket,bind ,listen 等方法,等会我们就会看到。我们这里不要和常说的socket(tcp udp这种)机制混淆即可。下图是unix工作机制的简单图解image.png
  5. 此时正常情况下 目标程序就已经进入监听状态,此时就可以向其发送 数据了,也就是我们代码中的:loadAgentimage.png

attach发起方基本就这些,但是,如果你不结合被attach来看很容易穿不起来,所以紧接着我们看下被attach的目标程序是如何实现的。之后我们总结并画个流程图就清晰了。

被attach的目标程序 源码分析

在 上边的步骤2 我们知道了 ,发起attach的程序会执行sendQutito方法最终会发送一个SIGQUIT信号给被attach的目标进程,那么目标程序是如何执行的呢?首先我们得知道既然发起了SIGQUIT信号,那么目标程序肯定得有监听 然后识别这个信号进行处理吧? 否则没有监听没处理那还怎么玩?而这个监听的线程是在jdk8u/hotspot/src/share/vm/runtime/thread.cpp 的 create_vm方法中创建的(create_vm我们上边有说过),如下: image.png image.png signal_thread_entry的代码比较重要,如下: (在 /jdk8u/hotspot/src/share/vm/runtime/os.cpp 中 )

// --------------------- sun.misc.Signal (optional) ---------------------


// SIGBREAK is sent by the keyboard to query the VM state
#ifndef SIGBREAK
#define SIGBREAK SIGQUIT ,对 SIGQUIT 进行 define  ,SIGBREAK 就代表 SIGQUIT信号
#endif

// sigexitnum_pd is a platform-specific special signal used for terminating the Signal thread.
static void signal_thread_entry(JavaThread* thread, TRAPS) {
  os::set_priority(thread, NearMaxPriority);
  //轮询
  while (true) {
    int sig;
    {
      // FIXME : Currently we have not decieded what should be the status
      //         for this java thread blocked here. Once we decide about
      //         that we should fix this.
      //进入等待 即 当前线程被block
      sig = os::signal_wait();
    }
    if (sig == os::sigexitnum_pd()) {
       // Terminate the signal thread
       return;
    }
    //一旦发现有信号,则退出block状态,进行下边处理
    switch (sig) {
      //如果是SIGQUIT信号 SIGBREAK其实就是SIGQUIT 因为他被defind: #define SIGBREAK SIGQUIT
      case SIGBREAK: {
#if INCLUDE_SERVICES
        
        // Check if the signal is a trigger to start the Attach Listener - in that
        // case don't print stack traces.
        //如果这个信号是用来触发并启动Attach Listener的,则不打印输出堆栈信息。
        //DisableAttachMechanism默认是false ,
        //此常量定义在了 : jdk8u/hotspot/src/share/vm/runtime/globals.hpp 中
        if (!DisableAttachMechanism) {
          // Attempt to transit state to AL_INITIALIZING.
          jlong cur_state = AttachListener::transit_state(AL_INITIALIZING, AL_NOT_INITIALIZED);
          if (cur_state == AL_INITIALIZING) {
            // Attach Listener has been started to initialize. Ignore this signal.
            //Attach Listener已经启动并初始化 忽略此信号
            continue;
          } else if (cur_state == AL_NOT_INITIALIZED) {
            // Start to initialize.
            //开始初始化 执行方法 :AttachListener::is_init_trigger()
            if (AttachListener::is_init_trigger()) {
              // Attach Listener has been initialized.
              // Accept subsequent request.
              //Attach Listenerq已经初始化完成,可以开始接收请求了
              continue;
            } else {
              // Attach Listener could not be started.
              // So we need to transit the state to AL_NOT_INITIALIZED.
              //没启动成功,设置状态为未启动
              AttachListener::set_state(AL_NOT_INITIALIZED);
            }
          } else if (AttachListener::check_socket_file()) {
            // Attach Listener has been started, but unix domain socket file
            // does not exist. So restart Attach Listener.
            //已经启动了Attach Listener ,但是没找到 unix 套接字文件,重启Attach Listener
            continue;
          }
        }
#endif
        //如果不是attach请求,则打印堆栈线程信息  也就是JVM对SIGQUIT信号的默认处理行为 比如 
        //kill -3 pid 这种。
        
        // Print stack traces
        // Any SIGBREAK operations added here should make sure to flush
        // the output stream (e.g. tty->flush()) after output.  See 4803766.
        // Each module also prints an extra carriage return after its output.
        VM_PrintThreads op;
        VMThread::execute(&op);
        VM_PrintJNI jni_op;
        VMThread::execute(&jni_op);
        VM_FindDeadlocks op1(tty);
        VMThread::execute(&op1);
        Universe::print_heap_at_SIGBREAK();
        if (PrintClassHistogram) {
          VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */);
          VMThread::execute(&op1);
        }
        if (JvmtiExport::should_post_data_dump()) {
          JvmtiExport::post_data_dump();
        }
        break;
      }
      default: {
        // Dispatch the signal to java 。  非SIGQUI信号 ,即其他信号的处理
        HandleMark hm(THREAD);
        Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD);
        KlassHandle klass (THREAD, k);
        if (klass.not_null()) {
          JavaValue result(T_VOID);
          JavaCallArguments args;
          args.push_int(sig);
          JavaCalls::call_static(
            &result,
            klass,
            vmSymbols::dispatch_name(),
            vmSymbols::int_void_signature(),
            &args,
            THREAD
          );
        }
        //异常处理 略
        }
      }
    }
  }
}

判断是否存在 /tmp/.attach_pid+pid文件!!!!!!!!!从注释即可看出来

// If the file .attach_pid<pid> exists in the working directory
// or /tmp then this is the trigger to start the attach mechanism
bool AttachListener::is_init_trigger() {
  if (init_at_startup() || is_initialized()) {
    return false;               // initialized at startup or already initialized
  }
  char path[PATH_MAX + 1];
  int ret;
  struct stat st;

//构建文件名称并判断
  snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d",
           os::get_temp_directory(), os::current_process_id());
  RESTARTABLE(::stat(path, &st), ret);
  
  //ret==0 说明存在
  if (ret == 0) {
    // simple check to avoid starting the attach mechanism when
    // a bogus user creates the file
    if (st.st_uid == geteuid()) {
     //初始化Attach Listener线程
      init();
      return true;
    }
  }
  //返回false说明 .attach_pid+pid文件不存在,不进行attach操作
  return false;
}



// The Attach Listener threads services a queue. It dequeues an operation
// from the queue, examines the operation name (command), and dispatches
// to the corresponding function to perform the operation.
static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
  os::set_priority(thread, NearMaxPriority);

  thread->record_stack_base_and_size();
  //AttachListener::pd_init()中会创建 绑定 监听
  if (AttachListener::pd_init() != 0) {
    AttachListener::set_state(AL_NOT_INITIALIZED);
    return;
  }
  AttachListener::set_initialized();

  for (;;) {
  //从队列取出 attach请求
    AttachOperation* op = AttachListener::dequeue();
    if (op == NULL) {
      AttachListener::set_state(AL_NOT_INITIALIZED);
      return;   // dequeue failed or shutdown
    }
    //进行处理 即 找到对应function进行处理调用。这里的代码略,一会我们截图看下
    
}

int AttachListener::pd_init() {
  JavaThread* thread = JavaThread::current();
  ThreadBlockInVM tbivm(thread);

  thread->set_suspend_equivalent();
  // cleared by handle_special_suspend_equivalent_condition() or
  // java_suspend_self() via check_and_wait_while_suspended()
  //init中会 : 创建,绑定,监听 unix套接字,之后就可以和发起attach方进行通信了
  int ret_code = BsdAttachListener::init();

  // were we externally suspended while we were waiting?
  thread->check_and_wait_while_suspended();

  return ret_code;
}

这段就是建立unix 连接,绑定,监听的代码了:

// Initialization - create a listener socket and bind it to a file

int BsdAttachListener::init() {
  char path[UNIX_PATH_MAX];          // socket file
  char initial_path[UNIX_PATH_MAX];  // socket file during setup
  int listener;                      // listener socket (file descriptor)

  // register function to cleanup
  if (!_atexit_registered) {
    _atexit_registered = true;
    ::atexit(listener_cleanup);
  }

  int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
                   os::get_temp_directory(), os::current_process_id());
  if (n < (int)UNIX_PATH_MAX) {
    n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
  }
  if (n >= (int)UNIX_PATH_MAX) {
    return -1;
  }

  // create the listener socket
  listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
  if (listener == -1) {
    return -1;
  }

  // bind socket
  struct sockaddr_un addr;
  addr.sun_family = AF_UNIX;
  strcpy(addr.sun_path, initial_path);
  ::unlink(initial_path);
  int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));
  if (res == -1) {
    ::close(listener);
    return -1;
  }

  // put in listen mode, set permissions, and rename into place
  res = ::listen(listener, 5);
  if (res == 0) {
    RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
    if (res == 0) {
      // make sure the file is owned by the effective user and effective group
      // (this is the default on linux, but not on mac os)
      RESTARTABLE(::chown(initial_path, geteuid(), getegid()), res);
      if (res == 0) {
        res = ::rename(initial_path, path);
      }
    }
  }
  if (res == -1) {
    ::close(listener);
    ::unlink(initial_path);
    return -1;
  }
  set_path(path);
  set_listener(listener);

  return 0;
}

上边就行被attach的目标程序的实现了,比较重要,总结一下

  1. 首先在create_vm阶段调用os::sigle_init,里边创建Signal Dispatcher线程,这个线程中(signal_thread_entry)会执行while(true)循环, 进入轮询状态
    • 之后进入阻塞等待(wait)等待接收各种: SIGNAL 信号
  2. 有信号后,进行下边的处理,
    • 如果是SIGQUIT信号,且存在tmp/.java_pid+"pid"文件,则认为是attach请求
      • 如果存在 tmp/.attach_pid+"pid"文件(在此方法中判断的:AttachListener::is_init_trigger())image.png 则认为是attach请求,则启动并初始化Attach Listener 线程,初始化时会进行 unix套接字的创建(socket),绑定(bind),监听(listen)image.png image.png image.png image.png
    • 如果是SIGQUIT信号但是不存在tmp/.attach_pid+"pid"文件,则进行打印输出 线程堆栈信息,也即JVM对SIGQUIT的默认处理,参见jstack这类的操作。如下所示:image.png

loadAgent ->请求目标程序加载agent jar

具体为发起attach的 这行代码:

vm.loadAgent("/Users/hzz/myself_project/xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar");

上边这个代码最终调用到了以下方法,就是向这个unix套接字写入具体的数据,目的就是要求 被attach进程,load 指定的agent jar 具体发送的内容就是 load instrument 你的jar路径 大概就是这个意思,如果想看详情使用strace 或 truss命令。 image.png

服务端accept并查找指定key对应的function

image.png funcs定义如下(注意从此方法可以看出:动态加载agent 能做的所有事情就是这些了):

// Table to map operation names to functions.

// names must be of length <= AttachOperation::name_length_max
static AttachOperationFunctionInfo funcs[] = {
  { "agentProperties",  get_agent_properties },
  { "datadump",         data_dump },
  { "dumpheap",         dump_heap },
  { "load",             JvmtiExport::load_agent_library },
  { "properties",       get_system_properties },
  { "threaddump",       thread_dump },
  { "inspectheap",      heap_inspection },
  { "setflag",          set_flag },
  { "printflag",        print_flag },
  { "jcmd",             jcmd },
  { NULL,               NULL }
};

因为我们在vm.laodAgnet(jarPath)时 laodAgnet内部最终传入的是 load 命令,也就是说会找JvmtiExport::load_agent_library这个函数,如下是JvmtiExport::load_agent_library对应的逻辑 image.png 另外因为laodAgnet中传入的链接库名称是instrument,(可以从laodAgnet源码看这里字数限制就不贴了)所以最终找到动态链接库就是libinstrument.so然后去执行 libinstrument.so的Agent_OnLoad方法,看到这里我想你应该明白后续的流程了,后边就和静态加载差不多了,如下: image.png

动态加载 Java Agent图解(重要重要重要)

如下图所示: image.png


5、一些自言自语

这篇文章酝酿至少好几个月其中被各种事情打断,另外也写了很久(断断续续一个月差不多)也是我有史以来最长的一篇,整个下来收获蛮多的,虽然java agent在实际直接开发中用的不多,但是并不代表他不重要,严格来讲我们每天都在使用依赖java agent开发的各种软件或框架。

有时候在想

  • 是什么支持我一点点扣这些底层源码的?
  • 是什么在我眼睛酸涩的情况下任然想打开电脑去深究?
  • 是什么驱使我 吃饭/地铁/睡前 都在想某个实现细节的?

是为了提升技术,是为了装13?是骨子里的执念?是对茅塞顿开的感觉上了瘾?我想这些都不重要了。 重要的是:我开心就好,仅此而已。

本文参考了 “你假笨” 大佬的文章和2篇官网文章:

http://lovestblog.cn/blog/2015/09/14/javaagent/ https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html https://www.ibm.com/docs/en/sdk-java-technology/8?topic=interfaces-jvmti


如果已经看到了这里的请帮忙😂,点个赞👍🏻👍🏻👍🏻。如果没有人看,那我就将此篇文章献给此刻迷茫的自己:孤芳独自赏,深情共白头。

2024.04.25