转至作者 蒋志伟:深入Java自动化探针技术的原理和实践

前言

建议阅读时间 30~40分钟

读者需要对Java JVM 一定了解,文章会系统的介绍Java 探针核心原理和技术实现,总结目前一些主流的框架方案。同时,接下来我会分享一篇关于 OpenTelemetry 开发Java 探针的文章,而OpenTelemetry 源代码的核心实现正好基于本篇的知识。

如果喜欢文章的内容,欢迎分享留言
文章涉及技术概念

JVMTI、Java Agent、ASM、Java Instrumentation、Byte Buddy、Javassist、JVM Attach、JPLISAgent、Java Byte-Code

JVMTI 技术

JVM在设计之初,就考虑到了虚拟机状态的监控、程序 Debug、线程和内存分析等功能。在JDK1.5 之前,JVM规范就定义了JVMPI(Java Virtual Machine Profiler Interface)也就是JVM分析接口以及JVMDI(Java Virtual Machine Debug Interface)也就是JVM调试接口,JDK1.5 以及以后的版本,这两套接口合并成了一套,也就是Java Virtual Machine Tool Interface,就是JVMTI 。通过JVMTI 可以探查JVM内部的一些运行状态,甚至控制JVM应用程序的执行。

JVMTI 大体声明支持以下功能:

1. Java Heap与GC获取所有类的信息,对象信息,对象引用关系,Full GC开始/结束,对象回收事件等。2. 线程与堆栈获取所有线程的信息,线程组信息,控制线程(start,suspend,resume,interrupt…), Thread Monitor(Lock),得到线程堆栈,控制出栈,方法强制返回,方法栈本地变量等。3. Class & Object & Method & Field 元信息Class信息,符号表,方法表,fields信息,Method信息等,Object信息。redefine class(hotswap), retransform class这里看到JVMTI 设计强大地方,他可以重新定义类对象!后面会聊到。4. 工具类线程CPU 消耗,ClassLoader路径修改,系统属性获取等

这里需要注意的是

  • • JVMTI是一套JVM的接口规范,不同的JVM实现方式可以不同,有的JVM提供了拓展性的功能,比如 openJ9,当然也可能存在JVM不提供这个接口的实现。

  • • JVMTI提供的是Native方式调用的API,也就是常说的JNI方式。JVMTI接口用C/C++的语言提供,最终以动态链接库的形式由JVM加载并运行 使用JNI方式调用JVMTI接口访问目标虚拟机的大体流程图

其中jvmti.h头文件中定义了JVMTI接口提供的方法,我们简单看看JDK 7 里面源代码一些重要方法

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/javavm/export/jvmti.h

  /*   64 : Get Method Name
(and Signature) */  jvmtiError (JNICALL *GetMethodName) (jvmtiEnv* env,    jmethodID method,    char** name_ptr,    char** signature_ptr,    char** generic_ptr);

  /*   65 : Get Method Declaring Class */  jvmtiError (JNICALL *GetMethodDeclaringClass) (jvmtiEnv* env,    jmethodID method,    jclass* declaring_class_ptr);

  /*   66 : Get Method Modifiers */  jvmtiError (JNICALL *GetMethodModifiers) (jvmtiEnv* env,    jmethodID method,    jint* modifiers_ptr);

JVMTI API 里序号 64、65 方法是从JVM中获取程序定义所有方法和相关类。66 比较有意思了,它获取修改后的方法,这说明借助JVMTI 可以动态修改Java 程序的方法

  /*   152 : Retransform Classes */  jvmtiError (JNICALL *RetransformClasses) (jvmtiEnv* env,    jint class_count,    const jclass* classes);

序号152 是一个非常重要的方法,JVMTI的 RetransformClasses函数来完成类的重定义过程,这也说明 JVMTI 可以修改Java程序整个类

// class_count - pre-checked to be greater than or equal to 0// class_definitions - pre-checked for NULLjvmtiError JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {//TODO: add locking  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);  VMThread::execute(&op);  return (op.check_error());} /* end RedefineClasses */

JVMTI 服务有两种打开方式:

  • • 在Java进程启动的时候通过 -agentpath:<path-to-agent>=<options>方式启动,path-to-agent对应的JVMTI 接口实现的动态库文件的绝对路径,后面可以追加JVMTI 程序需要的参数。Linux动态库文件的后缀为 .so

  • • 运行时挂载 Attach ,然后加载JVMTI 接口实现的动态库文件 JVMTI 的Agent、Attach 用C、C++编写,具体感兴趣想实践的朋友可以看看下面的例子

https://github.com/liuzhengyang/jvmti_examples

用C、C++实现JVMTI 功能对大部分Java 工程师的确强人所难。于是,Sun 公司出了 Java Agent,一个用Java 实现JVMTI 的方案,方案相当优雅和容易上手。

Java Agent 技术由来

Java Agent 直译为 Java 代理,中文圈也流行另外一个称呼 Java 探针 Probe 技术。

它在 JDK1.5 引入,是一种可以动态修改 Java 字节码的技术。Java 类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器

ClassFileTransformer 对这些字节码进行修改,以此来完成一些额外的功能。

Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作

//Java Agent 和目标进程一起启动模式java -javaagent:myagent.jar=mode=test Test

Agent 启动拦截提供两种方式:一种是程序运行前:在Main方法执行之前,通过一个叫 premain方法来执行

启动时需要在目标程序的启动参数中添加 -javaagent参数,Java Agent 内部通过注册 ClassFileTransformer ,这个转化器在Java 程序 Main方法前加了一层拦截器。在类加载之前,完成对字节码修改

Premain 完整工作流程图

另一种是程序运行中修改,需通过JVM中 Attach技术实现,Attach的实现也是基于 JVMTI

总结下,Java Agent 具备以下的能力

  • • Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;

  • • Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;

    Java Agent 的价值

Java Agent 成熟的技术架构,有着对字节码通用的重写能力。它应用场景是非常广泛

  • • IDE 的调试功能,例如 Eclipse、IntelliJ IDEA

  • • 热部署功能,例如 JRebel、XRebel、spring-loaded

  • • 各种线上诊断工具,例如 Btrace、Greys,国内阿里的 Arthas

  • • 各种性能分析工具,例如 Visual VM、JConsole 等

  • • 全链路性能检测工具,例如 OpenTelemetry、Skywalking、Pinpoint等 接下来,我们用案例实现性能检测工具的Java Agent 探针

Java Agent 和 JVMTI 关系

我们大致了解下Java Agent 底层源代码实现过程

首先弄清几个概念,我发现网上总结特别乱

JVMTIAgent

JVMTIAgent 是一个动态库,它可以利用JVMTI暴露出的一些接口来实现一些特殊的功能。我们常用Eclipse、Idea等IDE 工具代码调试就是利用它。Java Agent 也是利用了其中一个JVMTIAgent 来实现的。在Linux里面,这个库叫做libinstrument.so,在BSD系统中叫做libinstrument.dylib,该动态链接库在{JAVA_HOME}/jre/lib/目录下。因为源代码里面add_init_agent函数里面传递进去的是一个叫做 instrument的字符串,所以也称它为instrument 动态库,对应启动Agent 称为 Instrument Agent

Instrument 动态链接库

Instrument 支持使用Java Instrumentation API 来编写Java Agent。

Java Instrumentation : 在Jdk1.5 后,Java语言中提供的调用动态库的 Java API 接口 。

在Instrument 中有一个非常重要的类称为:JPLISAgent(Java Programming Language Instrumentation Services Agent),它的作用是初始化所有通过Java Instrumentation API编写的Agent。很容易猜到,Java Instrumentation API 其实就是底层调用JVMTI 。

JVMTIAgent 包含这个几个基本函数

JNIEXPORT jint JNICALLAgent_OnLoad(JavaVM *vm, char *options, void *reserved);

JNIEXPORT jint JNICALLAgent_OnAttach(JavaVM* vm, char* options, void* reserved);

JNIEXPORT void JNICALLAgent_OnUnload(JavaVM *vm); 
  • • Agent_OnLoad如果Agent 是在目标JVM 启动时加载(通过VM 参数 -agentpath:<path-to-agent>=<options>方式),在启动过程中会去执行Agent 里Agent_OnLoad函数

  • • Agent_OnAttach如果Agent 通过Attach 方法启动,执行Attach 的JVM会给目标JVM 进程发送Load 命令来加载 Agent,在加载过程中调用就是 Agent_OnAttach函数

  • • Agent_OnUnload在Agent 做卸载的时候调用 Instrument 实现了Agent_OnLoadAgent_OnAttach 两方法,所以Java Agent 既可以在JVM启动时,也就是加载 Java 字节码之前启动,也可以在 JVM 运行时启动,这很有价值。

大致画了一下 Java Agent 和 JVMTI 的关系

Java Instrument Package

上面提到实现Agent 需要 Instrument 动态链接库支持。Java 语言中也提供了调用动态库的 Java API 接口 Instrumentation。有了 Instrumentation,开发者可以轻松使用Java语言操作字节码,来实现Java Agent 相关功能,Instrument Package 大致结构

java.lang.instrument.*java.lang.instrument.Instrumentation;public interface Instrumentation {}

Instrumentation 工作原理

SUN工具包(sun.instrument.InstrumentationImpl)编写了一些Native 方法,JDK里提供了这些Native方法的实现类(jdk\src\share\instrument\JPLISAgent.c),通过JNI 方式访问JVMTI提供的方法,这些方法就是定义在jvmti.h头文件中。

Instrumentation 核心功能

Instrumentation 接口有一个最重要方法 addTransformer,它用于添加多个ClassFileTransformer。类似下面 Java Agent 实现的例子

ClassFileTransformer 中文类转换器,ClassFileTransformer提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。

Instrumentation 有一个TransformerInfo 数组保存ClassFileTransformer,像拦截器链表一样,顺序的进行字节码的重定义。

// 说明:添加ClassFileTransformer// 第一个参数:transformer,类转换器// 第二个参数:canRetransform,经过transformer转换过的类是否允许再次转换void Instrumentation.addTransformer(ClassFileTransformer transformer, boolean canRetransform)

// 说明:对类字节码进行增强,返回新的类字节码定义// 第一个参数:loader,类加载器// 第二个参数:className,内部定义的类全路径// 第三个参数:classBeingRedefined,待重定义/转换的类// 第四个参数:protectionDomain,保护域// 第五个参数:classfileBuffer,待重定义/转换的类字节码(不要直接在这个classfileBuffer对象上修改,需拷贝后进行)// 注:若不进行任何增强,当前方法返回null即可,若需要增强转换,则需要先拷贝一份classfileBuffer,在拷贝上进行增强转换,然后返回拷贝。byte[] ClassFileTransformer.transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte classfileBuffer)

下面我们写Java Agent时候,就会实现这两个重要的方法。通过 Instrument API 方式使用到了JVMTI提供部分功能,对开发者来说,主要提供的是对JVM加载的类字节码进行增强操作,等价于有了全局、动态修改Java程序代码的能力。

总结

到这里,是不是想到:我们是否可以通过Agent ,在所有类方法中插入额外的字节码,这些字节码功能就是获取程序内部数据,然后上报给某个地方。是的,我们很多通用的Java 监控、Debug、日志记录工具就是基于它来实现的。而且,插入的字节码是附加的,这些更变不会修改原来程序正常逻辑和状态,只是会有一些性能损耗,对应用程序本身基本是安全可靠的。

JVMTI 和 Java Instrument 对比

JVMTI方式 Java Instrument API
性能 独立进程,不受目标JVM影响 在目标JVM内,GC时会受到影响
功能性 方法众多,功能非常全面 字节码的操作
易用性 需要掌握C/C++,以及JNI开发相关知识 Java代码开发,上手快

Agent启动方式

程序运行前加载

目标JVM 启动时指定-javaagent:xxx.jar参数来启动 Java Agent, 这里 xxx.jar 是探针的JAR包. 比如 OpenTelemetry 运行Java 探针的指令

java -javaagent:path/to/opentelemetry-javaagent.jar \     -jar myapp.jar

程序启动时,优先加载Java Agent,执行里面的 premain方法。这个时候,其实大部分的类没有被加载。

Jar 打包规则

探针的 JAR包需要做以下配置

JAR包里MANIFEST.MF文件添加一个Premain-Class属性,指定一个实现了premain方法的类,加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项

MANIFEST.MF 大致如下配置

PreMain-Class: AgentMainCan-Redefine-Classes: trueCan-Retransform-Classes: true

premain 方法声明

// JVM启动时调用,其执行时Class 还未加载到JVMpublic static void premain(String agentArgs, Instrumentation inst);public static void premain(String agentArgs);

下面是我们实现premain 方法的一个测试类

Premain 方法工作原理

目标JVM 启动时,运行JNI 的 Agent_OnLoad 函数,执行如下步骤:

  • • 创建 InstrumentationImpl 对象

  • • 监听 ClassFileLoadHook 事件

  • • 调用 InstrumentationImpl 的 loadClassAndCallPremain 方法,此方法里去Agent Jar 包中找到MANIFEST.MF声明的Premain-Class类,执行类里面的 premain方法

  • • premain方法里面,我们可以调用Java Instrumentation API 完成字节码增强功能了

复习Java Byte-code 字节码概念

维基百科字节码中文解释

字节码(英语:Bytecode)通常指的是已经经过编译,但与特定机器代码无关,需要解释器转译后才能成为机器代码的中间代码。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。字节码的实现方式是通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接执行的指令。字节码的典型应用为Java bytecode

Java 程序运行原理

Java Byte-code: Java语言写出的源代码首先需要编译成class文件,即字节码文件,然后被JVM加载并运行,每个class文件具有如下固定的数据格式

ClassFile {    u4             magic;           // 魔数,固定为0xCAFEBABE    u2             minor_version;   // 次版本    u2             major_version;   // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推    u2             constant_pool_count;                     // 常量池个数    cp_info        constant_pool[constant_pool_count-1];    // 常量池定义    u2             access_flags;    // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等    u2             this_class;      // 类索引    u2             super_class;     // 父类索引    u2             interfaces_count;    u2             interfaces[interfaces_count];    u2             fields_count;    field_info     fields[fields_count];    u2             methods_count;    method_info    methods[methods_count];    u2             attributes_count;    attribute_info attributes[attributes_count];}

class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表。

字节码增强技术

Agent 本质是通过操作字节码,动态修改运行时Java对象。

我们把一类对现有字节码进行修改或者动态生成全新字节码文件的技术叫做字节码增强技术。

字节码增强技术的实现有很多方式,简单整理下目前比较成熟的一些操作字节码的框架

JDK动态代理运行期动态的创建代理类,只支持接口;

ASM一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解;

Javassist一个开源的分析、编辑和创建Java字节码的类库(源码级别的类库)。Javassist是Jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用Java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类;

Byte Buddy是一个较高层级的抽象的字节码操作工具,相较于ASM 而言。Byte Buddy 本身也是基于 ASM API 实现的。Byte Buddy以出色的性能,被著名的框架和工具(例如Mockito,Hibernate,Jackson,Google的Bazel构建系统等)使用

ASM

ASM 可以直接产生二进制.class文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为(也就是生成的代码可以覆盖原来的类也可以是原始类的子类)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

不过ASM在创建class 字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class 组织结构和JVM汇编指令有一定的了解。ASM提供了两组API: Core API 和Tree API,Core API是基于访问者模式来操作类的,而Tree是基于树节点来操作类的

简单写一个ASM 运行例子

public class ASMDemo extends ClassLoader{    public static <T> T getProxy(Class clazz) throws Exception {

        ClassReader classReader = new ClassReader(clazz.getName());        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);        classReader.accept(new ClassVisitor(ASM5, classWriter) {            @Override            public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {                // 方法过滤                if (!"hi".equals(name))                    return super.visitMethod(access, name, descriptor, signature, exceptions);                final MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);                return new AdviceAdapter(ASM5, methodVisitor, access, name, descriptor) {                    @Override                    protected void onMethodEnter() {                        // 执行指令;获取静态属性                        methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");                        // 加载常量 load constant                        methodVisitor.visitLdcInsn("方法名: "+name + "  你被代理了,By ASM!");                        // 在进入方法前,修改class,打印提示                        methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);                        super.onMethodEnter();                    }                };            }        }, ClassReader.EXPAND_FRAMES);        byte[] bytes = classWriter.toByteArray();        return (T) new ASMDemo().defineClass(clazz.getName(), bytes, 0, bytes.length).newInstance();    }}  

我们通过ASM动态代理一个简单的测试接口和实现类

public class HelloImpl implements Hello{    @Override    public String hi(String msg) {        return ("hello " + msg);    }}public interface Hello {    public String hi(String msg);}

写一个测试程序,通过ASM 代理模式,增强字节码后调用方法的效果

当然,基于ASM开发门槛比较高一些,你必须了解一定汇编原理和指令

Javassist

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

javassist 其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类;参考资料

http://www.javassist.org/https://github.com/jboss-javassist/javassist

下面我们有一个完整Java 探针实例用Javassist来实现

Byte Buddy

Byte Buddy是致力于解决字节码操作和 instrumentation API 的复杂性的开源框架。Byte Buddy 所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。通过使用 Byte Buddy,任何熟悉 Java 编程语言的人都容易地进行字节码操作

官网的示例展现了如何生成一个简单的类,这个类是 Object 的子类,并且重写了 toString 方法,用来返回“Hello World!”与原始的 ASM 类似,intercept 会告诉 Byte Buddy 为拦截到的指令提供方法实现

Class<?> dynamicType = new ByteBuddy()  .subclass(Object.class)  .method(ElementMatchers.named("toString"))  .intercept(FixedValue.value("Hello World!"))  .make()  .load(getClass().getClassLoader())  .getLoaded();System.out.println(dynamicType.getSimpleName());// 输出:Object$ByteBuddy$ilIxkTl1
Demo 来源 bytebuddy.net

字节码增强工具对比

框架 ASM Javassist JDK Proxy Cglib ByteBuddy
起源时间 2002 1999 2000 2011 2014
增强方式 字节码指令 字节码指令和源码(注:源码文本) 源码 源码 源码
源码编译 NA 不支持 支持 支持 支持
Agent支持 支持 支持 不支持,依赖框架 不支持,依赖框架 支持
性能
维护状态 停止升级 停止维护 活跃
优点 超高性能,应用场景广泛 同时支持字节码指令和源码两种增强方式 JDK原生类库支持 零侵入,提供良好的API扩展编程
缺点 字节码指令对应用开发者不友好 场景非常局限,只适用于Java接口 已经不再维护,对于新版JDK17+支持不好,官网建议切换到ByteBuddy
应用场景 小,高性能,广泛用于语言级别 广泛用于框架场景 广泛用于Trace场景

一个完整的Java Agent探针实现过程

目标

实现一个简单性能工具,通过探针统计Java程序所有方法的执行时间 1、构建 Maven 项目工程,添加 MANIFEST.MF , 目录大致

在 MANIFEST.MF文件中定义Premain-Class属性,指定一个实现类。类中实现了Premain方法,这就是Java Agent 在类加载启动入口

Manifest-Version: 1.0Premain-Class: com.laziobird.MyAgentDemoAgent-Class: com.laziobird.MyAgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true
  • • Premain-Class包含Premain方法的类

  • • Can-Redefine-Classes为true时表示能够重新定义Class

  • • Can-Retransform-Classes为true时表示能够重新转换Class,实现字节码替换 2、构建Premain方法

public class MyAgentDemo {    // JVM 启动时,Agent修改字节码    public static void premain(String args, Instrumentation inst) {        System.out.println(" premain agent loaded !");        inst.addTransformer(new PreMainTransformerDemo());        System.out.println(" agent addTransformer start !");    } ....   }

我们实现Premain方法类叫 MyAgentDemo,里面添加一个类转化器 PreMainTransformerDemo,这个转化器具体来实现统计方法调用时间 3、编写类转换器

在编写类转化器时,我们通过Javassist 来具体操作字节码,首先pom.xml 里面添加依赖

<dependency>   <groupId>org.javassist</groupId>   <artifactId>javassist</artifactId>   <version>3.25.0-GA</version></dependency>

接下来具体实现

public class PreMainTransformerDemo implements ClassFileTransformer{   final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";   final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";   @Override   public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                           ProtectionDomain protectionDomain, byte[] classfileBuffer){       // className 默认格式 com/laziobird 替换 com.laziobird       className = className.replace("/", ".");       //java自带的方法不进行处理,不是特别类的方法也不处理       if(className.startsWith("java") || className.startsWith("sun")|| !className.contains("com.laziobird")){           return null;       }       CtClass ctclass = null;       try {           // 使用全称,用于取得字节码类<使用javassist>           ctclass = ClassPool.getDefault().get(className);           for(CtMethod ctMethod : ctclass.getDeclaredMethods()){               String methodName = ctMethod.getName();               // 新定义一个方法叫做比如sayHello$old               String newMethodName = methodName + "$old";               // 将原来的方法名字修改               ctMethod.setName(newMethodName);               // 创建新的方法,复制原来的方法,名字为原来的名字               CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);               // 构建新的方法体               StringBuilder bodyStr = new StringBuilder();               bodyStr.append("{");               bodyStr.append("System.out.println(\"==============Enter Method: " + className + "." + methodName + " ==============\");");               //方法执行前,定义一个时间变量,记录方法开始前时间               bodyStr.append(prefix);               bodyStr.append(newMethodName + "($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数               //定义方法完成时间变量               bodyStr.append(postfix);               //方法完成后,运算方法执行时间               bodyStr.append("System.out.println(\"==============Exit Method: " + className + "." + methodName + " Cost:\" +(endTime - startTime) +\"ms " + "===\");");               bodyStr.append("}");               // 新方法字节码替换原来的方法字节码               newMethod.setBody(bodyStr.toString());               ctclass.addMethod(newMethod);// 增加新方法           }           //返回新的字节流           return ctclass.toBytecode();       } catch (Exception e) {           e.printStackTrace();       }       return null;   }

程序等价于:指定Java类下所有方法进行了如下转换,重新生成字节码加载执行

4、打包生成Java Agent的Jar 包

pom.xml配置好maven assembly,进行编译打包

5、写一个Java测试程序,验证探针是否生效

AgentTest 有两个简单方法testtestB

为了演示,其中testB 调用了另外一个类ClassC 的 methodD方法。

可以看到,类包名是 com.laziobird,刚才的Agent 只会对com.laziobird 的类起作用

package com.laziobird;public class AgentTest {    public void test() {        System.out.println("hello the method: agentTest.test ");    }    public void testB() {        ClassC c = new ClassC();        c.methodD();        System.out.println("hello the method: agentTest.testB ");    }    public static void main(String[] args) {        AgentTest agentTest = new AgentTest();        agentTest.test();        agentTest.testB();    }}package com.laziobird;public class ClassC {    public void methodD(){        try {            System.out.println(" methodD start!");            Thread.sleep(500);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }    }}

我们给测试程序打成可执行的Jar包,Jar 指定默认运行的类是 AgentTest

运行测试程序,通过-javaagent启动我们写的 Java Agent 探针

java -javaagent:/path/agentdemo/target/javaagent-demo-0.0.1-SNAPSHOT-jar-with-dependencies.jar  -jar  /path/gitproject/TestAgentDemo/out/artifacts/TestAgentDemo_jar/TestAgentDemo.jar

运行效果

程序运行时加载

在JDK1.6 版本中,Java Agent 支持了可以在JVM运行时动态修改 Java 字节码的能力。

这种能力需要JVM Attach 来实现

JVM Attach:简单来说就是JVM提供一种JVM进程间通信的机制。它能让一个进程传命令给另外一个进程,并让它执行内部的一些操作。

常见场景,比如做故障定位时,有可能我们觉得某些Java 的线程程序卡住了。于是想把一个JVM进程的线程Dump出来。那么我们会跑一个JStack的进程,然后传进程Id 参数,告诉Jstack指定哪个进程进行线程Dump。Attach 机制完成两个进程间如何通信和传输协议的定义

JVM Attach 实现原理

存在一个Attach Listener 线程,监听其他JVM的Attach 请求,其通信方式基于socket,JVM Attach机制底层从Kernel 到 Application 层完整流程图

具体C语言源代码实现,可以参考李嘉鹏这篇深入分享

http://lovestblog.cn/blog/2014/06/18/jvm-attach/?spm=ata.13261165.0.0.26d52428n8NoAy

Agentmain 工作原理

Java Agent在运行时和启动时加载机制其实很像,主要区别在Agent 进行字节码增强前,对于拦截入口不同而已。一个叫Premain,一个叫Agentmain 。这一点很好理解:启动时,Agent 直接通过启动参数-javaagent吸附于当前JVM 进程。运行时加载,其实当前JVM进程已经启动了。这时借助另一个JVM进程通信,调用Attach API 再把Agent 启动起来。后面的字节码修改和重加载的过程那就是一样的。

运行时Java Agent 配置

和启动时代理类似:

  • • JAR包的MANIFEST.MF清单文件中定义Agent-Class属性,指定一个实现类。加入Can-Redefine-Classes和 Can-Retransform-Classes 选项

  • • JAR包中包含清单文件中定义的这个类,类中包含agentmain方法,方法逻辑自己实现 JAR包内对应的MANIFEST.MF有如下配置

Agent-Class: com.laziobird.MyAgentDemoCan-Redefine-Classes: trueCan-Retransform-Classes: true

需要注意:agentmain方式由于是采用Attach 机制,被代理的目标程序VM 已经先于Agent 启动,其所有类已经被加载完成。这个时候需要执行 Instrumentation 的 retransformClasses方法让类进入重新转换,重定义的过程:它会激活类执行ClassFileTransformer列表中的回调,完成字节码操作,最后让类加载器重新加载

Attach Agentmain 和 PreMain 对比

1、字节码增强的限制

虽然运行时Agent可以在JVM 运行时动态的修改某个类的字节码,但是为了保证JVM的正常运行,新定义的类相较于原来的类需要满足:

父类是同一个实现的接口数也要相同,并且是相同的接口类访问符必须一致字段数和字段名要一致新增或删除的方法必须是private static/final的可以修改方法内部代码

2、显式调用重定义方法 因为JVM启动时,字节码 Class文件已经提前生成好再进行Class Load过程。但是JVM 运行后,把已经加载后的类动态修改 redefine an already loaded class,我们修改完类定义后,显式在内存中进行重加载 reload Class

Java Agent 显式给我们提供了retransformClasses方法,下面我摘取它的详细说明

void retransformClasses(Class<?>... classes) {}/**Returns whether or not the current JVM configuration supports redefinition of classes. The ability to redefine an already loaded class is an optional capability of a JVM. Redefinition will only be supported if the Can-Redefine-Classes manifest attribute is set to true in the agent JAR file (as described in the package specification) and the JVM supports this capability. During a single instantiation of a single JVM, multiple calls to this method will always return the same answer.**/

一个基于Attach的Java Agent 探针实现过程

目标

实现一个简单性能工具,通过Java Agent 探针统计Java应用程序下所有方法的执行时间

1、还是之前 Maven 项目工程,在 MANIFEST.MF 文件中定义Agentmain-Class属性,指定一个实现类。类中实现了Agentmain方法,这就是Java Agent 在JVM运行时加载的启动入口

Agent-Class: com.laziobird.MyAgentDemo

2、构建Agentmain方法

public class MyAgentDemo {  // JVM运行时,Agent修改字节码  public static void agentmain(String args, Instrumentation inst) {      System.out.println(" agentmain agent loaded !");      Class[] allClass = inst.getAllLoadedClasses();      for (Class c : allClass) {          inst.addTransformer(new AgentMainTransformerDemo(), true);          try {          //agentmain 是JVM运行时,需要调用 retransformClasses 重定义类 !!          inst.retransformClasses(c);               } catch (UnmodifiableClassException e) {                 throw new RuntimeException(e); }          }    } ....   }

我们在类 MyAgentDemo实现agentmain方法,里面添加一个类转化器 AgentMainTransformerDemo,这个转化器插入实现统计方法调用时间的字节码片段

public class AgentMainTransformerDemo implements ClassFileTransformer {    @Override    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {        className = className.replace("/", ".");        //这次我们用另外一种简洁API方法修改字节码        if (className.contains("com.laziobird")) {            try {                // 得到类信息                CtClass ctclass = ClassPool.getDefault().get(className);                for (CtMethod ctMethod : ctclass.getDeclaredMethods()) {                    // 方法内部声明局部变量                    ctMethod.addLocalVariable("start", CtClass.longType);                    // 方法前插入Java代码片段                    ctMethod.insertBefore("start = System.currentTimeMillis();");                    String methodName = ctMethod.getLongName();                    ctMethod.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +                            ".currentTimeMillis() - start));");                    // 方法结束尾部插入Java代码片段                    return ctclass.toBytecode();                }            } catch (Exception e) {                e.printStackTrace();            }        }        return null;    }}

3、重新打包生成新的Jar包 运行 maven assembly,进行编译打包

4、写测试的Java程序

类AgentAttachTest 定义一个方法,为了方便查看Attach 效果,我们让JVM 主进程一直循环执行这个方法。同时为了区分,通过随机数改变方法的运行时间。这样看到探针每次统计结果也不同。类的包名是com.laziobird,Agent 只会对com.laziobird 的类起作用

public void test(int x) {    try {        long sleepTime = x*1000;        Thread.sleep(sleepTime);        System.out.println("the method: AgentAttachTest.test | sleep time = " + sleepTime+ "ms");    } catch (InterruptedException e) {        throw new RuntimeException(e);    }}public static void main(String[] args) {    AgentAttachTest agentTest = new AgentAttachTest();    while (1==1){        int x = new Random().nextInt(10);        agentTest.test(x);    }}

5、编写一个演示 Attach 通信的JVM 程序,用于启动 Agent

public class AttachJVM {    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {        // 获取运行中的JVM列表        List<VirtualMachineDescriptor> vmList = VirtualMachine.list();        // 我们编写探针的Jar包路径        String agentJar = "/Users/jiangzhiwei/eclipse-workspace/agentdemo/target/javaagent-demo-0.0.1-SNAPSHOT-jar-with-dependencies.jar";        for (VirtualMachineDescriptor vmd : vmList) {            // 找到测试的JVM            System.out.println("vmd name: "+vmd.displayName());            if (vmd.displayName().endsWith("AgentAttachTest")) {                // attach到目标ID的JVM上                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                // agent指定jar包到已经attach的JVM上                virtualMachine.loadAgent(agentJar);                virtualMachine.detach();}}}}

运行效果

1、运行测试的Java程序,为了方便,也可以不用打成Jar运行

2、我们启动Attach 的JVM程序。它主要动作:

1、通过Attach API,找到要监听的JVM进程,我们称为VirtualMachine

2、VirtualMachine 借助Attach API 的LoadAgent方法将Agent 加载进来

3、Agent 开始工作!我们回过头来看看探针在测试程序的运行效果

我们手写Java 探针在JVM 运行时也能动态改变字节码。

Github 案例地址

为了方便大家上手实践,我贡献案例到Github,其实基于Java Agent 性能诊断工具、链路分析的Java 探针基本都是类似实现,大部分区别在于字节码增强实现的差异。

当然,要求更高的性能和底层功能,可以直接编写C、C++的JVMT 动态链接库。

https://github.com/laziobird/java-agent-demo

探针实现

https://github.com/laziobird/java-agent-demo/tree/main/Agentdemo

测试程序

https://github.com/laziobird/java-agent-demo/tree/main/TestAgentDemo

Attach 用例

https://github.com/laziobird/java-agent-demo/tree/main/JVMAttach

本文的教程

深入Java自动化探针技术的原理和实践相关推荐

  1. 《VMware 网络技术:原理与实践》—— 3.2 以太网

    本节书摘来自华章出版社<VMware 网络技术:原理与实践>一 书中的第3章,第3.2节,作者:(美)Christopher Wahl Steven Pantol,更多章节内容可以访问云栖 ...

  2. Java agent 探针技术(1)-JVM 启动时 premain 进行类加载期增强

    文章目录 1. 简介 2. 使用 Java agent 的步骤 3. 使用示例 3.1 创建实现 ClassFileTransformer 接口的类 3.2 创建使用 ClassFileTransfo ...

  3. Java Agent 探针技术

    Java 中的 Agent 技术可以让我们无侵入性的去进行代理,最常用于程序调试.热部署.性能诊断分析等场景,现如今比较火热的分布式链路追踪项目Skywalking,就是通过探针技术去捕获日志,将数据 ...

  4. java图像识别算法_图像算法原理与实践——绪论

    本系列文章是写给程序源的数字图像处理教程,从最基础的知识来讲解数字图像处理专业知识,通过最基本的编码方式来实践相应的处理算法,从而使得大家掌握基础的图像处理知识. 关于图像处理知识,在高校课程中,比较 ...

  5. 【JAVA基础☞探针技术】Java探针-Java Agent技术

    个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 1.原理:基于javaAgent和Java字节码注入技术的java探针工具技术原理 2.原理分析 动态代理功能实现说明,我们利用ja ...

  6. Java远程通讯技术及原理分析

    在分布式服务框架中,一个最基础的问题就是远程服务是怎么通讯的,在Java领域中有很多可实现远程通讯的技术,例如:RMI.MINA.ESB.Burlap.Hessian.SOAP.EJB和JMS等,这些 ...

  7. Java 远程通讯技术及原理分析

    转自:https://www.cnblogs.com/Luouy/p/7399918.html 消息模式 归根结底,企业应用系统就是对数据的处理,而对于一个拥有多个子系统的企业应用系统而言,它的基础支 ...

  8. Java Agent探针技术

    1.基本概念 Java Agent 是 jdk1.5 引入的特征,此特征为用户提供了在 jvm 将字节码文件读入内存后,jvm 使用对应的字节流在 java 堆中生成 Class 对象之前,用户可以对 ...

  9. java对象工厂池_[转载]Java对象池技术的原理及其实现

    作者:汪永好 出处:计算机与信息技术 责任编辑: 方舟 [ 2006-11-25 07:00 ] 摘 要 :本文在分析对象池技术基本原理的基础上,给出了对象池技术的两种实现方式.还指出了使用对象池技术 ...

最新文章

  1. C++模拟游戏中鼠标点击和键盘按键
  2. 他花了一个月,使用MicroPython将自己装进OLED里面
  3. (Eclipse)(STM32) STM32在Eclipse編程
  4. 1、user权限表详解
  5. java是如何写入文件的
  6. docker 操作 记录
  7. python进程的状态及创建
  8. matlab 三维绘图 抛光,瓷砖抛光过程建模与仿真
  9. BSOD 又见BSOD。。。。
  10. iOS 中delegate的理解与使用(传值)
  11. 后台开发笔记-在服务器上运行java后台项目
  12. 安卓获取浏览器上网记录_Android 获取自带浏览器上网记录
  13. 谷歌账户跑着跑着没点击了,跑不出去什么原因。
  14. 2329: 小新同学爱加密
  15. 关于《网上购书系统》
  16. vue项目引入百度地图BMapGL鼠标绘制和BMap辅助工具
  17. 什么时候不要采用微服务架构
  18. PHP处理iso8583报文
  19. 历史上的今天:苹果电脑之父诞生;阿里巴巴收购雅虎中国;OpenAI 击败电竞世界冠军...
  20. html页面漏斗图,echarts 漏斗图示例

热门文章

  1. 视频质量评价基础与实践
  2. python实现数据结构(1)
  3. MongoDB——牛X的索引操作
  4. django 1.8 官方文档翻译: 13-9-1 如何使用会话
  5. 如何把Dragonfly从一台电脑移到另外一台电脑
  6. 微信小程序商城 php,Thinkphp3.2微信小程序商城源码
  7. 基于 Web 引擎技术的 Web 内容录制
  8. 实现图像特效之浮雕与雕刻
  9. GPS实时定位、获取基站信息
  10. 配置开源安卓QQ协议库Mirai