快速入门-Java Instrumentation

378 阅读7分钟
# 官方文档
<https://docs.oracle.com/en/java/javase/21/docs/api/java.instrument/java/lang/instrument/Instrumentation.html>

# JVM 虚拟机字节码指令表
<https://segmentfault.com/a/1190000008722128>

# 参考
<https://www.baeldung.com/java-instrumentation>

1. Java Instrumentation

Java Instrumentation 是 Java 语言提供的一种功能,用于在运行时动态修改字节码,允许开发者在应用程序运行时对类进行插桩(Instrumentation)或增强(Enhancement)。它是 Java 语言中实现 APM(应用性能监控)、性能优化、代码覆盖率分析、热修复等技术的基础。通过 Instrumentation API,开发者可以在不修改源代码的情况下,通过字节码操作动态调整应用行为。

在大型应用中,修改代码重新部署往往需要耗费较多时间,而动态插桩可以在不重新启动应用的情况下进行调整。在应用运行时插入性能监控逻辑,无需改变源代码即可采集数据,如响应时间、内存使用情况。为开发者提供了一种安全、可控的机制,以动态修改字节码,而不直接操作底层 JVM。Instrumentation 是诸如代码覆盖率工具(如 JaCoCo)、性能分析工具(如 JProfiler)、应用监控工具(如 New Relic、AppDynamics)等技术的基础。

2. Java Instrumentation 学习

2.1 快速实践

如何使用java.lang.instrument 包通常与 Java Agent 一起使用。Java Agent 是一个特殊的 Java 程序,通常在 JVM 启动时通过 -javaagent 参数指定,或者在运行时通过某些 API 注入到 JVM 中。

Java Agent 示例

  1. 创建一个 Java Agent:首先,我们需要创建一个类,该类实现 java.lang.instrument.Agent 接口,来定义类加载时的字节码转换。

    import java.lang.instrument.Instrumentation;
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    
    public class MyAgent {
    
        // 这个方法将在 JVM 启动时调用
        public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("Agent started");
            // 添加字节码转换器
            inst.addTransformer(new MyTransformer());
        }
    
        public static class MyTransformer implements ClassFileTransformer {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain protectionDomain, byte[] classfileBuffer) {
                // 这里可以修改字节码,当前示例只是打印类名
                System.out.println("Class loaded: " + className);
                return classfileBuffer; // 不修改字节码,直接返回
            }
        }
    }
    
  2. 编译并打包 Agent:将 MyAgent 类编译并打包成一个 JAR 文件,确保该 JAR 文件包含一个 MANIFEST.MF 文件,并且声明了 Premain-Class 属性,指明了入口类:

    Manifest-Version: 1.0
    Premain-Class: MyAgent
    
  3. 在运行时使用 Agent:在启动 Java 应用程序时,通过 -javaagent 参数加载 Agent:随便找一个可以启动的jar引用,使用 -javaagent 参数加载 Agent

    java -javaagent:/path/to/your-agent.jar -jar your-application.jar
    

4.输出效果:启动应用时,Agent 会打印所有加载的类名。例如:

Agent started
Class loaded: java/lang/String
Class loaded: java/util/ArrayList

字节码修改(使用 Instrumentation 类)

import java.lang.instrument.*;
import java.security.ProtectionDomain;

// 在 Java Agent 中,Instrumentation 类的 redefineClasses() 方法可以用来重新定义已加载的类的字节码。这对于修补、替换或增强现有类非常有用。
public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Agent started");

        // 添加字节码转换器
        inst.addTransformer(new MyTransformer());

        // 重新定义已加载的类(如果需要)
        try {
            Class<?> clazz = MyClass.class;
            byte[] newByteCode = modifyClassByteCode(clazz);
            ClassDefinition classDefinition = new ClassDefinition(clazz, newByteCode);
            inst.redefineClasses(classDefinition);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static byte[] modifyClassByteCode(Class<?> clazz) {
        // 在此实现字节码的修改逻辑
        return null; // 返回修改后的字节码
    }

    public static class MyTransformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            System.out.println("Class loaded: " + className);
            return classfileBuffer; // 不修改字节码,直接返回
        }
    }
}

2.2. Java Instrumentation 详解

Java 源代码的编译过程如下图:

image.png

JVM的运行时数据区的结构如下图:

image.png

字节码指令执行的数据结构是栈帧(Stack Frame),也就是在虚拟机栈中的栈元素。

JVM 虚拟机会为每个方法分配一个栈帧,因为虚拟机栈是LIFO(后进先出)的,所以当前线程正在活动的栈帧,也就是栈顶的栈帧,JVM规范中称之为“CurrentFrame”, 这个当前栈帧对应的方法就是“CurrentMethod”。

字节码的执行操作,指的就是对当前栈帧数据结构进行的操作。

Java Instrumentation 的实现 Agent Code

image.png

关键类和接口 java.lang.instrument 包中的主要类和接口如下

# Instrument 接口

		Instrument 是一个接口,定义了许多方法,用于实现字节码的转换、插件的安装等功能。Instrument 接口的实例通过 Java Agent 传递,并提供了对运行时的访问权限。

		方法:
			getAllLoadedClasses():      获取所有加载的类
			isModifiableClass(Class<?>):检查某个类是否可被修改
			transform(ClassLoader, String, Class<?> , ProtectionDomain, byte[]):字节码转换方法,允许修改类的字节码

# ClassFileTransformer 接口

		ClassFileTransformer 是一个接口,用于定义字节码转换方法。开发者可以通过实现这个接口来修改类的字节码。
		
		方法:
			transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer):该方法用于转换类的字节码
		

# Instrumentation 类

		Instrumentation 是核心类,它提供了对 JVM 运行时的操作能力,主要用于字节码的转换和类加载的控制。它允许动态修改类的字节码并重新定义类。
		
		常用方法:
			getObjectSize(Object object):                    获取指定对象的内存占用大小(仅支持 Java 8 及以上版本)。
			redefineClasses(ClassDefinition... definitions): 重新定义已加载的类的字节码。
			addTransformer(ClassFileTransformer transformer):向 JVM 添加字节码转换器,用于在类加载时对类进行字节码修改。
			removeTransformer(ClassFileTransformer transformer):移除字节码转换器。
			isModifiableClass(Class<?> clazz):检查一个类是否可以被修改。
			
			
# ClassDefinition

		ClassDefinition 类用于表示类的定义,主要包括类的字节码和类对象。这对于重新定义已加载的类是必不可少的。
		
		构造方法:
			ClassDefinition(Class<?> clazz, byte[] classFile):用于通过字节码重新定义类。

2.3 总结

特性/库Java InstrumentationASMByteBuddyCGLIBJavassist
功能字节码修改、类重新定义字节码操作与生成字节码增强与生成继承代理字节码操作与生成
使用场景AOP、性能监控、动态更新字节码增强、性能优化动态代理、AOP、字节码增强动态代理(无接口)字节码修改、动态生成
易用性复杂,低级别控制低级,复杂简洁,流式API简单,继承代理简单,API 简洁
性能较高,依赖 JVM 的优化高效,但底层控制复杂高效且灵活较高,继承代理性能较好,适合动态生成
优点可与 JVM 完全集成,动态增强类高性能,控制精确高层次抽象,易用不需要接口,灵活性高API 简洁,易用
缺点使用复杂,学习成本高API 难用,底层操作性能较 ASM 慢,但更易用不支持接口代理较少的控制,效率较低
典型应用AOP、性能监控动态代理、代码增强动态代理、字节码增强AOP、动态代理(无接口)代码增强、类生成

Java Instrumentation 提供了在运行时修改字节码的能力,使开发者能够通过代理、字节码增强、性能监控、动态分析等方式来提升应用程序的功能和性能。具体来说,java.lang.instrument 提供了对字节码操作的能力,主要包括:

  • 类的字节码增强:在类加载时修改其字节码,可以进行性能优化、日志记录、方法拦截等。
  • 动态类定义和重新定义:允许在运行时对已加载类的字节码进行重新定义,适用于热更新等场景。
  • 性能分析:能够动态监控对象的内存占用、方法调用次数、执行时间等,帮助开发者优化性能。
  • 调试与日志:可以在不改变源代码的情况下增加调试信息、日志记录等功能。

解决的问题:

  • 性能瓶颈分析:在不改变源代码的情况下,动态地监控方法执行时间、对象内存占用等。
  • 代码动态增强:通过动态修改字节码来增强现有类的功能,常用于 AOP(面向切面编程)框架。
  • 类的热更新:在运行时重新定义类,实现代码的热部署和升级,避免重启应用。
  • 运行时监控:可以在运行时动态注入代码进行性能监控、日志记录等。

更多内容欢迎关注 [ 小巫编程室 ] 公众号,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。好好学习,天天向上(good good study day day up)。