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

news2024/12/23 9:05:19

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

本文大概结构: - 前置节:简单认识 ->JVMTI,Java Agent,JVMTIAgent,libinstrument.so (先混个脸熟) - 第一节:Java Agent介绍与(静/动)态加载方式描述+图解。 - 第二节:JVMTI介绍,功能&使用场景以及c语言自实现一个JVMTIAgent - 第三节:Java Agent 静态加载demo+源码分析+图解 - 第四节:Java Agent 动态加载demo+源码分析+图解 - 第五节:自言自语 - 另外由于本文字数限制,所以关于Java Agent的 实际应用 另写了一篇:使用Java Agent 插桩技术:实现 DB中敏感字段自动加解密

本文涉及到的知识点:

  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(“要重新加载的类”);来触发。 1.  使用Attach API: Attach API允许一个运行中的Java进程连接(通过UNIX套接字)到另一个Java进程。一旦连接,它可以用来加载Agent JAR。这通常通过使用 com.sun.tools.attach.VirtualMachine类实现,该类提供了附加到目标JVM进程并加载Agent的方法 1.  加载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内发生的特定情况的通知,如线程启动/结束、类加载/卸载、方法进入/退出等。 1. 线程管理:它提供了监控和管理Java程序中线程状态的能力。 1. 堆和垃圾回收:JVMTI支持查询堆信息、监控垃圾回收事件,以及在某些条件下控制垃圾回收的执行。 1. 调试支持:JVMTI为调试器提供了丰富的接口,支持断点、单步执行、字段访问/修改等调试功能。 1. 性能监测:提供了监视和分析JVM性能的工具,如获取方法执行时间、内存使用情况等。

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

文字描述你可能感觉不到什么,但是如果提到这些框架,你大概率会知晓其中的一个或者几个,而他们就是基于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: ```c
include
include
include
// 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命令来进行编译:  bash 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文件,如下:  bash 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代码如下:

    
       
          org.apache.maven.plugins
          maven-compiler-plugin
          
             

11 11 org.apache.maven.plugins maven-assembly-plugin 2.4.1 jar-with-dependencies 黄壮壮 ${maven.build.timestamp} com.xzll.agent.config.JdkDateAgentTest true false false make-assembly package single

使用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方法的第三个参数 是false

image.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
 *  [=]. 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 [=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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1709997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

瑞_Windows环境下使用bat重启jar包等服务

文章目录 命令示例重启ray-project.jar重启redis服务 &#x1f64a; 前言&#xff1a;经验分享——Windows环境下使用.bat批处理文件重启 jar 包等服务。在学习或者工作日常中&#xff0c;有时候会需要在 Windows 系统环境下去启动 jar 包或其它服务&#xff0c;此时如果使用关…

百世慧入选第七届数字中国建设峰会“2024企业数字化转型典型应用案例”

5月24日-25日&#xff0c;第七届数字中国建设峰会在福州举行。本届峰会是国家数据工作体系优化调整后首次举办的数字中国建设峰会&#xff0c;主题为“释放数据要素价值&#xff0c;发展新质生产力”。 为了全方位展示各领域数字化最新成果&#xff0c;共创数字中国美好未来&a…

mail发送调用接口如何与三方服务无缝对接?

mail发送调用接口的性能怎么样&#xff1f;调用邮件接口的技巧&#xff1f; 为了提高效率和自动化水平&#xff0c;企业通常会选择使用mail发送调用接口。然而&#xff0c;仅仅使用这些接口还不够&#xff0c;如何与各种第三方服务无缝对接同样至关重要。AokSend将探讨如何有效…

Golang性能分析工具pprof--远程分析时无法定位源代码行数问题解决方案

场景 通过命令行模式的list命令&#xff0c;为了查看指标消耗在具体哪一行&#xff0c;需要源代码。但实际程序是部署在线上或者程序的源代码目录变了&#xff0c;则pprof从默认路径找不到代码&#xff0c;无法显示是哪一行的问题。 通过浏览器模式的source页面&#xff0c;有…

Linux java jni调用C++封装动态库

由于项目中java需要调用第三方提供的C动态库&#xff1b;由于第三方动态库传入的参数较多&#xff0c;还伴随着指针传入操作&#xff0c;导致java调用极为不便&#xff01;因此催生出对于第三方的C动态库进行二次封装。java调用只需按结构传入一个结构化的string即可。话不多说…

狂暴少帅短视频:成都科成博通文化传媒公司

狂暴少帅短视频&#xff1a;热血与激情的碰撞 在当下这个信息爆炸的时代&#xff0c;短视频以其独特的魅力迅速占领了人们的视线。而在众多短视频创作者中&#xff0c;一位名为“狂暴少帅”的创作者以其独特的风格和引人入胜的内容&#xff0c;赢得了广大网友的喜爱和追捧。今…

关于pdfbox读取pdf

最近&#xff0c;想着将pdf的文件进行读取其内容&#xff0c;发现了一个比较好用的依赖pdfbox。目前使用这个依赖&#xff0c;进行实现一个简单实例&#xff0c;如果之后需要使用到更深的了解&#xff0c;会进行更新。这里提醒一下&#xff1a;jdk8尽量采用pdfbox3.x版本。 对…

怎样查看JavaScript中没有输出结果的数组值?

在JavaScript中&#xff0c;可以方便地定义和使用数组&#xff0c;对于已经定义的数组&#xff0c;怎样查看其值呢&#xff1f; 看下面的示例&#xff0c;并运行它。 上面的示例中&#xff0c;标签不完整&#xff0c;请补充完整再试运行。你知道少了什么标签么&#xff1f; 注…

react ant 表格实现 拖拽排序和多选

项目背景 : react ant 要实现 : 有多选功能(实现批量删除 , 也可以全选) 可以拖拽(可以复制 , 方便顶部的搜索功能) 要实现效果如下 1 这是最初的拖拽功能实现 , 不能复制表格里的内容 , 不符合要求 2 更改了ROW的内容 , 实现了可以复制表格内容 代码 //控制是否可以选中表格…

拉普拉斯IPO:科技与产业深度融合,实现业务领域延展

我国拥有全球最具竞争优势的光伏产业链&#xff0c;基于降本增效的需求&#xff0c;光伏产业对于技术革新具有持续的需求。拉普拉斯新能源科技股份有限公司&#xff08;以下简称“拉普拉斯”&#xff09;凭借深厚的技术积累&#xff0c;以及对光伏产业深刻的理解&#xff0c;聚…

GitLab的安装及基础操作

1. 项目目标 &#xff08;1&#xff09;熟练使用rpm包安装gitlab &#xff08;2&#xff09;熟练配置gitlab &#xff08;3&#xff09;熟练创建gitlab群组、成员、项目 &#xff08;4&#xff09;熟练使用gitlab推送和拉取代码 2. 项目准备 2.1. 规划节点 主机名 主机I…

数据结构初阶 栈

一. 栈的基本介绍 1. 基本概念 栈是一种线性表 是一种特殊的数据结构 栈顶&#xff1a;进行数据插入和删除操作的一端 另一端叫做栈底 压栈&#xff1a;插入数据叫做压栈 压栈的数据在栈顶 出栈&#xff1a; 栈的删除操作叫做出栈 出栈操作也是在栈顶 栈遵循一个原则 叫做…

人脸检测--FaceNet(四)

FaceNet 是一个由 Google 研究团队开发的人脸识别系统&#xff0c;它基于深度学习技术&#xff0c;可以实现高精度的人脸识别、验证和聚类任务。FaceNet 通过学习直接从图像像素到人脸嵌入的映射&#xff0c;使得它在各种人脸识别任务中表现出色。下面是对 FaceNet 的详细介绍&…

python探索时钟模拟之旅:从设计到实现

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、设计时钟类 三、代码实现 四、扩展功能&#xff1a;指定步数后自动停止 五…

编程中的模块迷宫:区分与正确使用

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、模块混淆的陷阱 二、碳模块与探母模块的区别 三、如何正确使用模块 四、代码示例 五…

【Linux】数据链路层协议+ICMP协议+NAT技术

欢迎来到Cefler的博客&#x1f601; &#x1f54c;博客主页&#xff1a;折纸花满衣 &#x1f3e0;个人专栏&#xff1a;Linux 目录 &#x1f449;&#x1f3fb;数据链路层&#x1f449;&#x1f3fb;以太网以太网帧格式网卡Mac地址对比ip地址 &#x1f449;&#x1f3fb;MTUMTU…

9、C#【进阶】特性

特性 文章目录 1、特性概念2、自定义特性 Attribute3、特性的使用4、限制自定义特性的使用范围5、系统自带特性1、过时特性2、调用者信息特性3、条件编译特性4、外部dll包函数特性 1、特性概念 特性是一种允许我们向程序的程序集添加元数据的语言结构 它是用于保存程序机构信息…

5、xss-labs之level6

一、level6-----大小写绕过 1、测试分析 测试了之前用过的payload&#xff0c;发现都不行&#xff0c;并且level4使用的Java伪协议也不行&#xff0c;可以得出<>、script、onclick都被过滤 2、构造payload 因为href被过滤&#xff0c;可以试一下大写HREF 初试payload…

【debug】windows11安装WSL+Docker+本地部署cvcat

windows系统安装wsl虚拟机 首先观察是否已启用虚拟化&#xff1a; 在windows应用商店下载wsl 下载好后打开&#xff0c;创建用户名和密码&#xff0c;即可使用&#xff1a; 换源&#xff1a;ubuntu | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirr…

双机多网口配置同网段地址,可以通过目的IP确定接收数据的网卡吗?

环境 两台机器两网卡同网段接入同一个二层交换机。 机器A ens38 00:0c:29:a4:8b:fb 10.0.0.11/24 ens39 00:0c:29:a4:8b:05 10.0.0.12/24 机器B ens38 00:0c:29:4f:a6:c4 10.0.0.21/24 ens39 00:0c:29:4f:a6:ce 10.0.0.22/24 初始ARP表 只有管理口接口的ARP表项&#xff0c…