likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 70

前言

本文大概结构:

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

本文涉及到的知识点:

0、前置说明

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

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

  • 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三个我们比较关注的函数,截图如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析 当我们静态加载agent jar(启动时添加vm参数 -javaagent:xxxjar包路径的方式)时Agent_OnLoad会调用到我们的premain方法,当我们动态加载(JVM的attach机制,通过发送load命令来加载)时Agent_OnAttach会调用到我们的agentmian方法。也许你现在不明白这个,但是当你看了下边的第三&四节源码后你就能串起来了。

  • Instrumentation API: 为Java Agent提供了一套Java层面的接口,它是在Java 5开始引入的,旨在为Java开发者提供一种标准方式来动态修改类的行为以及做增强操作,部分示例:关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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

以上几个知识点之间的关系图如下: ps:牢记这几个知识点之间的关系与各自的功能,会使我们理解本文起到事半功倍的效果!!!关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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

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

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

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

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

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

关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 可以看到通过在 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方法一览: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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 命令打包

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

解压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文件内容: 关于Java Agent的使用、工作原理、及hotspot源码 解析

编写&执行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

也就是我们所说的: 静态加载关于Java Agent的使用、工作原理、及hotspot源码 解析 看下效果: 关于Java Agent的使用、工作原理、及hotspot源码 解析 可以看到,在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得认识吧?所以就来到了 解析参数这一步,解析参数的入口在这里: 关于Java Agent的使用、工作原理、及hotspot源码 解析 接下来到 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方法的第三个参数 是false关于Java Agent的使用、工作原理、及hotspot源码 解析 而这个参数就是 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的原因。如下是构建动态连接文件的代码截图: 关于Java Agent的使用、工作原理、及hotspot源码 解析

之后就是加载动态链接文件,然后就是寻找OnLoad也就是上边提到的find_agent_function ,最终会将找到的动态连接文件中的Agent_OnLoad方法保存到一个entry中并返回,之后就是执行动态链接库中的Agent_OnLoad方法了也即上边已经说过的代码: 关于Java Agent的使用、工作原理、及hotspot源码 解析 到此,寻找动态链接库以及执行动态链接库中的方法就分析完了

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

而实际上 libinstrument.so 这个动态链接库的实现是位于java/instrumentat/share/native/libinstrument 入口的InvocationAdapter.c 我们不妨来简单看下: 关于Java Agent的使用、工作原理、及hotspot源码 解析

在上边的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,截图如下:

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

接下来就是通过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方法,如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 我去,终于看到自己写的代码了,不容易啊。翻山越岭的。

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

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

关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 调用我们MAINFEST.MF Premain-Class类中的premain方法并传入参数(包括启动时-javaagent:xxjava.jar=option1=value1=option2=value2传入的参数和Instrumentation的实例对象) 关于Java Agent的使用、工作原理、及hotspot源码 解析

调用到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方法: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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

而jvm中类加载是从这个地方开始的(systemDictionary.cpp): 关于Java Agent的使用、工作原理、及hotspot源码 解析 因为我们自己编写的类都是要通过系统类加载器加载的,所以会走到这个系统类加载,我们继续跟,来到classLoader.cpp中的 load_calassfile方法,如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析

类加载时回调在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: 关于Java Agent的使用、工作原理、及hotspot源码 解析 jvmtiExport.cpp -> post_all_envs: 关于Java Agent的使用、工作原理、及hotspot源码 解析 jvmtiExport.cpp -> post_all_envs中的 post_to_env: 关于Java Agent的使用、工作原理、及hotspot源码 解析 上边方法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修改 方法如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

在调用完DefineTransformer类的transform方法后,从上边可以看到返回了修改后的字节码,需要将修改后的类数据添加到类文件流,使得修改后的内容生效呀(最终加载到元空间的是我们在DefineTransformer类transform方法 修改后的内容),所以就有了下边的代码: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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


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

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

关于Java Agent的使用、工作原理、及hotspot源码 解析 上图简单语言概括下:

  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.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的入口)

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

编写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文件(用到哪些就开启哪些,不用的最好关掉)

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

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

打包并查看xzll/study-agent/target/study-agent-0.0.1-SNAPSHOT-jar-with-dependencies/META-INF/MANIFEST.MF中的内容,如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

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

编写发起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服务器:

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

javac 编译并运行 AttachTarget

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

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

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

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

关于Java Agent的使用、工作原理、及hotspot源码 解析 可以看到,通过动态加载成功插桩到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类),不同系统有不同的实现,如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析 其实最好是以linux为例(因为我们的demo就是在linux系统上演示的),源码: 关于Java Agent的使用、工作原理、及hotspot源码 解析 但是因为我安装的是mac版本的jdk,所以我的java源码中的实现只有bsd系统(mac底层是基于bsd)的实现,即: BsdAttachProvider ,所以这里我们以bsd系统的 BsdAttachProvider 为例(值的注意的是BsdAttachProvider 和 LinuxAttachProvider 基本上一致 所以这里也就不纠结非得看LinuxAttachProvider的源码了): 关于Java Agent的使用、工作原理、及hotspot源码 解析

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了,这个逻辑一会再源码可以看到),这个路径的来源可以参考下边的源码截图:

    • 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析
  2. 然后发送 SIGQUIT 信号给目标进程,(在 sendQuitTo(var3);这行代码)

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

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

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

被attach的目标程序 源码分析

在 上边的步骤2 我们知道了 ,发起attach的程序会执行sendQutito方法最终会发送一个SIGQUIT信号给被attach的目标进程,那么目标程序是如何执行的呢?首先我们得知道既然发起了SIGQUIT信号,那么目标程序肯定得有监听 然后识别这个信号进行处理吧? 否则没有监听没处理那还怎么玩?而这个监听的线程是在jdk8u/hotspot/src/share/vm/runtime/thread.cpp 的 create_vm方法中创建的(create_vm我们上边有说过),如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 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())关于Java Agent的使用、工作原理、及hotspot源码 解析 则认为是attach请求,则启动并初始化Attach Listener 线程,初始化时会进行 unix套接字的创建(socket),绑定(bind),监听(listen)关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析 关于Java Agent的使用、工作原理、及hotspot源码 解析
    • 如果是SIGQUIT信号但是不存在tmp/.attach_pid+"pid"文件,则进行打印输出 线程堆栈信息,也即JVM对SIGQUIT的默认处理,参见jstack这类的操作。如下所示:关于Java Agent的使用、工作原理、及hotspot源码 解析

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命令。 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

关于Java Agent的使用、工作原理、及hotspot源码 解析 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对应的逻辑 关于Java Agent的使用、工作原理、及hotspot源码 解析 另外因为laodAgnet中传入的链接库名称是instrument,(可以从laodAgnet源码看这里字数限制就不贴了)所以最终找到动态链接库就是libinstrument.so然后去执行 libinstrument.so的Agent_OnLoad方法,看到这里我想你应该明白后续的流程了,后边就和静态加载差不多了,如下: 关于Java Agent的使用、工作原理、及hotspot源码 解析

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

如下图所示: 关于Java Agent的使用、工作原理、及hotspot源码 解析


5、一些自言自语

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

有时候在想

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

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

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

lovestblog.cn/blog/2015/0… docs.oracle.com/javase/8/do… www.ibm.com/docs/en/sdk…


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

2024.04.25

转载自:https://juejin.cn/post/7361564628952399899
评论
请登录