字节码增强要点总结

news/2025/3/4 15:45:55/文章来源:https://www.cnblogs.com/mazhimazhi/p/18603189

我们写的Java源代码需要通过Javac等前端编译器转换为Java字节码,虚拟机加载了这些Java字节码后,可以通过解释执行或JIT编译执行转换为本地机器码进行执行,如下图所示。

Java虚拟机在加载时认的是Java字节码,而动态类加载可以让我们在需要的时候生成字节码并加载到虚拟机中。不过我们不但能加载新生成的类,而且借助Instrumentation类能够修改已经加载的类,这让字节码增强有了无限遐想的空间。

如上的话说白了就是,你可以在不需要重启虚拟机,不需要Java源代码的情况下,任意更改Java字节码的逻辑并且让它生效。

1. 字节码增强API

在字节码增强中,我们需要知道两个类,一个是ClassFileTransformer,这个类定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理,譬如进行字节码增强;还有一个是Instrumentation,其中定义了非常重要的API,如下: 

public interface Instrumentation {// 增加一个Class 文件的转换器,转换器用于改变Class二进制流的数据,// 参数 canRetransform 设置是否允许重新转换
// 在对一个类注册了该转换器后,未来该类的每一次redefine以及retransform,都会被该转换器检查到,并且执行该转换器的操作void addTransformer(ClassFileTransformer transformer, boolean canRetransform);void addTransformer(ClassFileTransformer transformer);//删除一个类转换器boolean removeTransformer(ClassFileTransformer transformer);//是否允许对class retransformboolean isRetransformClassesSupported();// 在类加载之后,重新定义Class// 该方法是1.6之后加入的,事实上,该方法是update了一个类
// 用于对已经加载的类进行插桩,并且是从最初类加载的字节码开始重新应用转换器,并且每一个被注册到JVM的转换器都将会被执行void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;//是否允许对class重新定义boolean isRedefineClassesSupported();// 此方法用于替换类的定义,而不引用现有的类文件字节,就像从源代码重新编译// 以进行修复和继续调试时所做的那样。// 在要转换现有类文件字节的地方(例如在字节码插装中),应该使用retransformClasses。// 该方法可以修改方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,// 也不能修改方法的签名void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;//获取已经被JVM加载的class,有className可能重复(可能存在多个classloader)@SuppressWarnings("rawtypes")Class[] getAllLoadedClasses();// 获取某个对象的(字节)大小,注意嵌套对象或者对象中的属性引用需要另外单独计算long getObjectSize(Object objectToSize);// 将某个jar加入到Bootstrap Classpath里优先其他jar被加载void appendToBootstrapClassLoaderSearch(JarFile jarfile);// 将某个jar加入到Classpath里供AppClassloard去加载void appendToSystemClassLoaderSearch(JarFile jarfile);// 设置某些native方法的前缀,主要在找native方法的时候做规则匹配void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);// 是否支持设置native方法的前缀boolean isNativeMethodPrefixSupported(); }

如上的这些方法是字节码增强过程中经常使用的。

举个例子,现在要增强自己写的业务代码,只需要:

MyTransformer monitor = new MyTransformer();
inst.addTransformer(monitor, true);

 然后编写Transformer,如下:

public class MyTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader classLoader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] bytes) {// 增强逻辑}
}

注册一个Transformer,从此之后的类加载都会被Transformer拦截。ClassFileTransformer的transform()方法可以直接对类的字节码进行修改,但是只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性。

大概的流程如下图所示。

假设要增强已经被系统加载了的类(因为虚拟机在启动时就会预先加载一些基本的系统类),比如要增强java.lang.Thread类,那此时可能需要retransformClasses()类了,如下:

if (inst.isRetransformClassesSupported()) {Class<?>[] classes = inst.getAllLoadedClasses();for (Class<?> c : classes) {if (c.getName().contains("java.lang.Thread")) {if(inst.isModifiableClass(c)){try {inst.retransformClasses(c);} catch (UnmodifiableClassException e) {e.printStackTrace();}break;}}}
}

不过也不是什么类都可以被增强的,像基本类型对应的封装类型,如java.lang.Integer是不能被增强的,调用对应Class对象的isModifiableClass()方法会返回false,如果强行进行retransformClasses(),会抛出UnmodifiableClassException异常。retransformClasses()方法对已经加载的类重新触发类加载,然后使用addTransformer()方法注册的ClassFileTransformer重新对类进行修饰。

retransformClasses()方法对已经加载的类重新触发类加载,然后使用addTransformer()方法注册的ClassFileTransformer重新对类进行修饰。应用场景如下:第一,在执行premain()或agentmain()方法前,JVM早已加载了不少类,而这些类的加载事件并没有被拦截,因此也没有被注入。使用retransformClasses()方法可以注入这些已加载但未实现增强的类。第二,在定义了多个Java Agent的情况下,有时候可能需要调用removeTransformer()方法需要移除其中的部分注入,然后调用retransformClasses()方法重新从原始byte数组开始进行注入。需要注意的是,新加载的类不能修改旧有类声明,譬如不能增加属性、不能修改方法声明

在对Java的native方法进行增强时,由于native方法在Java层面没有方法体,所以需要使用setNativeMethodPrefix()和isNativeMethodPrefixSupported()等完成。

在增强业务类或系统类时,如果在增强的类中引用JavaAgent中的类时,通常会引起ClassNotFoundException,如下面的MyTransformer类中引用了specpower包下的Stats类,此时需要将JavaAgent这个jar包通过appendToBootstrapClassLoaderSearch()方法追加根类加载器搜索路径。如果JavaAgent中的类很多,会严重污染目标用户的业务类,甚至引起干扰。可通过BTrace那样,将整个JavaAgent的类名改一个唯一的名称,或者像Arthas一样,使用间谍类Spy来解决。

下面是引起ClassNotFoundException异常的原因图示。

 

2. 字节码增强工具

不过Java字节码并不同于Java源代码,可读性肯定要差,修改起来难度不低,所以大家一直在为降低这个难度而努力着。。。,到目前为止,已经有不少第三方工具包可以使用了,我用表格列表了一下常用的三方工具包,如下:

ASM 不受任何限制进行字节码修改,效率相对高一些,要求掌握字节码指令  操作难度大,编写代码多 Arthas、BTrace等,值得一提的是,Javaassit和ByteBuddy的底层实现用了ASM
Javaassit Java原始语法,书写相对直观,不要求使用者掌握字节码指令 性能比ASM稍差 Fastjson、MyBaties
ByteBuddy

支持任意维度的拦截,可以获取原始类、方法以及代理类和全部参数,

提供了丰富的API,无需掌握字节码指令

性能比ASM稍差 SkyWalking、Mockito

其实字节码工具在选择时,最看重的2个点就是的易用和性能好,一般线上的监控产品大多都会选择ASM,因为性能好。

Java的字节码是以栈为基础运行的,如果我们要直接更改这些字节码,除了要对指令熟悉外,还需要关注局部变量表、操作数栈的一致性等等问题,所以最好借助字节码工具来降低难度。尤其是能降低一些切面逻辑编写的难度,也就是在方法前,方法后等特定点上插入自己的增强逻辑。

3. JavaAgent

现在认识了字节码增强的API,有了字节码修改时辅助的工具,现在还有个问题,就是怎么将我们要修改的逻辑挂在目标应用程序上呢?这就要讲一下JavaAgent了,这个JavaAgent可以通过在启动时或者运行时动态挂载到目标应用程序上。

在启动时,使用-javaagent命令,调用到JavaAgent的premain()方法

在运行时,使用VirtualMachine类的attach()方法挂载,调用到JavaAgent的agentmain()方法

通常我们为了兼容两种启动模式,可以都实现一下premain()和agentmain()方法,如下:

public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) {instrument(agentArgs,inst);}public static void agentmain(String agentArgs, Instrumentation inst){instrument(agentArgs,inst);}private static void instrument(String agentArgs, Instrumentation inst){MyTransformer monitor = new MyTransformer();inst.addTransformer(monitor, true); }}

需要提示的是,在执行premain()或agentmain()方法前,JVM早已加载了不少类,而这些类的加载事件并没有被拦截,因此也没有被注入。如果要对这些已经加载的类进行增强,需要使用retransformClasses()方法。

JavaAgent的出现,极大的解耦了增强实现和目标应用程序的耦合程度,为后来的一大批做监控、做可观测以及做各种性能剖析的商业化公司提供了技术上的落地实现。想像一下,如果客户开发的某个应用程序需要稳定性保障,你做为一个监控产品的提供者,你可以给用户这样说,“我们产品不需要你的程序修改任何一行代码,甚至不需要重启应用程序就可以具备监控功能”。现在的商业公司提供的监控产品并不局限在Java生态,无论用户用的是什么技术栈,什么操作系统,什么架构,都能够自动识别并监控。

JavaAgent的实现基于JVMTI,我们还可以编写JVMTIAgent,不过需要用C/C++等语言编写,它能调用JNI接口以及订阅JVMTI事件等,比JavaAgent更加强大。

4. 字节码增强注意点

字节码增强需要注意的点有几个:

(1) 重定义可能会更改方法体、常量池和属性。重定义不得添加、移除、重命名字段或方法。

(2) 如果重定义的方法有活动的堆栈帧,那么这些活动的帧将继续运行原方法的字节码。将在新的调用上使用此重定义的方法。

(3)一个程序可能会挂载许多JavaAgent,这可能会导致前一个JavaAgent增强过的逻辑被后面JavaAgent增强时擦写,导致前一个增强逻辑失效;或者前一个JavaAgent出现阻塞,导致后续的JavaAgent没有正确加载,所以谨慎安排JavaAgent的挂载顺序,可参考实例:https://www.cnblogs.com/huaweiyun/p/16876537.html

(4)字节码增强本身的一些致使Bug也不少,这些Bug严重到会导致JVM Crash,而且有些Bug很不好浮现,不好追踪

(5)会影响到目标程序

(6)javaagent的premain和agentmain的类是通过应用类加载器加载的,所以如果要和业务代码通信,需要考虑classloader不同的情况,一般要通过反射(可以传入指定classloader加载类)和业务代码通信,或者可以学习Arthas那样用间谍类来解决

(7)注意依赖冲突的问题,比如agent的fatjar中包含了某个第三方的类,业务代码中也包含了相同的第三方但是不同版本的类,由于classloader存在父类优先委派加载的情况,可能会导致类加载异常,所以一般会通过shaded修改第三方类库的包名或者通过classloader隔离,也可以对整个三方包进行名称更改,maven是支持做这样的操作的

一是JavaAgent代码会影响到目标程序,例如不小心用到了目标应用程序的日志框架,导致JavaAgent相关的数据打印到了目标应用程序的日志里,所以一定要做好类隔离,可以像BTrace进行改名,也可以像Arthas那样用Spy间谍类避免;

二是JavaAgent会影响目标程序的效率,这可能是因为可能增强时插入的逻辑过多,涉及到增强的方法过多等因素造成,还可能会造成逆优化,也就是已经被JIT编译好的方法会被弃掉。所以对于商业化监控系统来说,一定要考虑Trace Agent的自适应采集设计,如何自动调节采集频率以及采集比例控制能力等性能开销优化。

关于最后一点影响目标应用程序的执行效率,需要我们特别关注。JavaAgent是附着在目标应用程序上执行的,JavaAgent中对任何全局变量等的修改都会影响到目标应用程序,假设JavaAgent写了一个会导致JVM Crash的Bug,那么目标应用程序就会Crash。

5. 小实例

现在我们要编写一个JavaAgent并统计一下方法valueOf在不同参数下的调用情况,如下:

public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) {instrument(agentArgs,inst);}public static void agentmain(String agentArgs, Instrumentation inst){instrument(agentArgs,inst);}private static void instrument(String agentArgs, Instrumentation inst){MyTransformer monitor = new MyTransformer();inst.addTransformer(monitor, true); JarFile f = null;try {// 生成的jar路径,为的是让BigDecimal类中valueOf方法顺利找到Stats类f = new JarFile(生成的jar路径);} catch (IOException e) {e.printStackTrace();}inst.appendToBootstrapClassLoaderSearch(f);
}

在编写Agent时,有两个方法premain()和agentmain(),如果使用-javaagent参数启动Agent,则调用premain(),如果使用VirtualMachine类的attach()方法挂载则调用agentmain()。

接下来我们编写MyTransformer,如下:

public class MyTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader classLoader,String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] bytes) {try {if (className == null) {return null;}if (!className.equals("java/math/BigDecimal")) {return null;}String currentClassName = className.replaceAll("/", ".");CtClass ctClass = ClassPool.getDefault().get(currentClassName);CtBehavior[] methods = ctClass.getDeclaredBehaviors();for (CtBehavior method : methods) {if ("valueOf".equals(method.getName())) {String sig = method.getSignature();if ("(JI)Ljava/math/BigDecimal;".equals(sig)) {method.insertBefore("specpower.Stats.valueOf($1,$2);");}}}return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}return null;}
}

使用javassist来改写字节码,所以要把相关的jar包引入项目中。对BigDecimal的valueOf(long unscaledVal,int scale)方法进行增强,也就是在方法体的开始处插入一段调用代码:

specpower.Stats.valueOf(unscaledVal,scale);

在Stats类中统计相关的信息,如下:

package specpower;...public class Stats {public static AtomicInteger a = new AtomicInteger(0);public static AtomicInteger b = new AtomicInteger(0);public static void valueOf(long val, int scale) {if (val == 0 && scale == 2) {a.incrementAndGet();} else if (val == 1 && scale == 0) {b.incrementAndGet();}}
}

统计当参数为0,2或1,0时,valueOf的调用次数,在SPECPower退出时打印。

在MyAgent中注册钩子函数:

Runtime.getRuntime().addShutdownHook(new Thread(){public void run(){Stats.print();}
});

 在Stats中打印即可,如下:

public static void print() {System.out.println(a.get() + "  " + b.get());
} 

 打JavaAgent的jar包时,一定要在MANIFEST.MF文件中指定如下内容:

Agent-Class: specpower.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: specpower.MyAgent

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ragflow-ollama 知识库建立测试

ollama查看模型 C:\Users\DK>ollama show deepseek-r1:7bModelarchitecture qwen2parameters 7.6Bcontext length 131072embedding length 3584quantization Q4_K_MParametersstop "<|begin▁of▁sentence|>"stop &…

北大手册第Ⅲ版已公开,带你深度学习DeepSeek-R1推理模型!

北大手册第Ⅲ版已公开,带你深度学习DeepSeek-R1推理模型!随着DeepSeek的全球风靡和广泛应用,智能化的普及步伐显著加快。通过对算法、模型和系统的系统级协同创新,DeepSeek汇聚了众智与众力,创造了许多精彩成果。为了更好的使用DeepSeek-R1大模型,使其能够为我们提供更专…

delphi 协程,全面开启 新的主流开发方式,多线程转向 - 协程开发,跟上主流的步伐

前言 golang依靠 协程 大败 Java,让Java 长时间以来 难以想到方案,至今也没有做出 成熟的 协程模型解决方案,有的人 误解以为 协程 仅仅是开发服务端的人 才会用到,这个完全是误解,协程是一种解决问题的思路转变, 客户端 和 服务端 都可以使用协程来开发,用协程几句代码…

lua符号

__ 注释符号

供应链中的的“四流合一”

供应链的四流就是人们常说的物流、商流、资金流和信息流。这篇文章,我们来学习一下供应链中的“四流合一”到底是什么。在供应链中,物流、资金流、信息流、商流是共同存在的,商流、信息流和资金流的结合将更好的支持和加强供应链上、下游企业之间的货物、服务往来(物流)。…

摆烂重新学markdown

Markdown学习 首先呢,博客园呢,右边的编辑器是可以选择Markdown编辑文章的,刚刚百度百科查到的 然后呢,预览可以看看你写的文章能呈现出来的效果 再然后呢,ctrl+s可以保存,写一点保存一点吧 好的,那就开始摆烂写垃圾吧 1.大标题怎么写呢? 大标题===#+空格+内容文字 2.2级标题怎…

[2025.3.1 JavaWeb学习]Maven高级

分模块设计将不同的功能块分开开发设计,而后只需要引入依赖即可使用继承与聚合 继承

Deepseek开源啦,R1模型可以部署本地使用,完全免费还能断网使用,感兴趣的朋友可以尝试一下

下载Ollama 下载地址:https://ollama.com/ 下载后根据显卡性能选择对应大小的R1模型,额...我的是1.5b 终端/cmd,执行命令,本地运行模型 ollama run deepseek-r1:1.5b 可使用2种办法快捷使用配合VS cord插件Continue使用 安装插件后Add Chat model,选择本地模型 这时候就可以…

【硬件测试】基于FPGA的256QAM基带通信系统开发与硬件片内测试,包含信道模块,误码统计模块,可设置SNR

1.算法仿真效果 本文是之前写的文章:《基于FPGA的256QAM基带通信系统,包含testbench,高斯信道模块,误码率统计模块,可以设置不同SNR》的硬件测试版本。在系统在仿真版本基础上增加了ila在线数据采集模块,vio在线SNR设置模块,数据源模块。硬件ila测试结果如下:(完整代码运行…

V90通过工艺对象在1200上的使用

配置CU参数打开V-Assistant,新建工程选择驱动选择电机型号选择控制模式V90伺服定位控制方式分为两种,一种是通过工艺对象,另外一种是通过EPOS标准块: a.如果选用工艺对象控制伺服电机,控制模式选用速度控制。报文选用标准报文3. b.如果选用EPOS标准块控制伺服电机,控制模式…