【JVM】字节码与ASM字节码增强、Instrument实现类的动态重加载
目录
- 字节码与ASM字节码增强
- 什么是字节码?
- 字节码结构
- 操作数栈与字节码
- 字节码增强
- ASM
- 运行时类加载
- Instrument
- JPDA与JVMTI
- instrument实现热加载的过程
字节码与ASM字节码增强
以下内容摘自:字节码增强技术探索
什么是字节码?
- Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用
- 之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。
在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图所示。
- 编译:由javac编译,实现.java文件编译为.class文件(字节码文件)
- 加载:JVM加载,读取.class文件,并且由classLoader加载
- 校验:bytecode verifier是JVM中对字节码进行校验的部分
- 执行:由解释器执行;部分热点代码由JIT即时编译器编译为本地代码执行。
字节码结构
- 常量池
常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区
操作数栈与字节码
- JVM的指令集是基于栈而不是寄存器,基于栈可以具备很好的跨平台性(因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。
字节码增强
- 字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术
ASM
- 对于需要手动操纵字节码的需求,可以使用ASM,应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等
它可以直接生产 .class字节码文件
也可以在类被加载入JVM之前动态修改类行为
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:- ClassReader:用于读取已经编译好的.class文件。
- ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
- 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
直接使用ASM实现AOP
- 利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。
- 首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。
public class Base {public void process(){System.out.println("process");}
}
- MyClassVisitor类,用于对字节码的visit以及修改
MyClassVisitor继承自ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;public class MyClassVisitor extends ClassVisitor implements Opcodes {public MyClassVisitor(ClassVisitor cv) {super(ASM5, cv);}@Overridepublic void visit(int version, int access, String name, String signature,String superName, String[] interfaces) {cv.visit(version, access, name, signature, superName, interfaces);}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法if (!name.equals("<init>") && mv != null) {mv = new MyMethodVisitor(mv);}return mv;}class MyMethodVisitor extends MethodVisitor implements Opcodes {public MyMethodVisitor(MethodVisitor mv) {super(Opcodes.ASM5, mv);}@Overridepublic void visitCode() {super.visitCode();mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("start");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}@Overridepublic void visitInsn(int opcode) {if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)|| opcode == Opcodes.ATHROW) {//方法在返回之前,打印"end"mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("end");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}mv.visitInsn(opcode);}}
}
- Generator类,在这个类中定义ClassReader和ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;public class Generator {public static void main(String[] args) throws Exception {//读取ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);//处理ClassVisitor classVisitor = new MyClassVisitor(classWriter);classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);byte[] data = classWriter.toByteArray();//输出File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");FileOutputStream fout = new FileOutputStream(f);fout.write(data);fout.close();System.out.println("now generator cc success!!!!!");}
}
- 运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class文件进行查看,可以看到反编译后的代码已经改变了。然后写一个测试类MyTest,在其中new Base(),并调用base.process()方法,可以看到下图右侧所示的AOP实现效果:
运行时类加载
- 在上文中我们避重就轻将ASM实现AOP的过程分为了两个main方法:
第一个是利用MyClassVisitor对已编译好的class文件进行修改
第二个是new对象并调用
这期间并不涉及到JVM运行时对类的重加载,而是在第一个main方法中,通过ASM对已编译类的字节码进行替换,在第二个main方法中,直接使用已替换好的新类信息。 - 如果我们在一个JVM中,先加载了一个类,然后又对其进行字节码增强并重新加载会发生什么呢?
即在增强前就先让JVM加载Base类,我们会发现它是在最后调用了ClassLoader的native方法defineClass()时报错。也就是说,JVM是不允许在运行时动态重载一个类的。
- 那如何解决JVM不允许运行时重加载类信息的问题呢?
为了达到这个目的,我们接下来一一来介绍需要借助的Java类库。
Instrument
- instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现。
- 要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform()方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
JPDA与JVMTI
- JPDA(Java Platform Debugger Architecture)
如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。正如JDPA名称中的Debugger,JDPA其实是一套用于调试Java程序的标准,任何JDK都必须实现该标准。 - JPDA定义了一整套完整的体系,它将调试体系分为三部分,并规定了三者之间的通信接口。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:
- JVM TI(JVM TOOL INTERFACE,JVM工具接口)
是JVM提供的一套对JVM进行操作的对外接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。
而Agent就是JVMTI的一种实现,Agent有两种启动方式,一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。
使用场景
- 至此,字节码增强技术的可使用范围就不再局限于JVM加载类前了。通过上述几个类库,我们可以在运行时对JVM中的类进行修改并重载了。通过这种手段,可以做的事情就变得很多了:
- 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。
- Mock:测试时候对某些服务做Mock。
- 性能诊断工具:比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。
instrument实现热加载的过程
步骤有两个
- addTransformer(ClassFileTransformer transformer, boolean canRetransform)
注册Transformer - retransformClasses(Class<?>… classes) throws UnmodifiableClassException
触发类重新加载,从而使得注册的类修改器能够重新修改类的字节码
retransformClasses方法
- 替换后在下一个invoke中生效:如果一个被修改的方法已经在栈桢中存在,则栈桢中的会使用旧字节码定义的方法继续运行,新字节码会在新栈桢中执行
- 不修改变量值:该方法不会导致类的一些初始化方法执行、不会修改静态变量的值
- 重新定义的类的实例不受影响
(对象实例持有着指向方法区的类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。) - 只改变方法体:该方法可以改变类的方法体、常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名
- 字节码有问题时不加载:在类转化前该方法不会check字节码文件,如果结果字节码出错了,该方法将抛出异常。如果该方法抛出异常,则不会重新定义任何类
方法实现的原理
- ClassFileLoadHook
虚拟机提供的类文件加载钩子函数,在虚拟机创建初始化时注册并监听。
在调用retransformClasses之后,其中有一个步骤是从文件流中解析类,触发此事件。 - Transformer
在jvmtiEventClassFileLoadHook的处理过程中,最终将调用InstrumentationImpl的transform方法,再进入到我们自定义的Transformer类的transform方法,返回处理后的字节码,替换原来的byte[],然后再进入类的解析阶段
VMThread
- 运行时类字节码替换,整体的执行依赖于VMThread,VMThread是一个在虚拟机创建时生成的单例原生线程,这个线程能派生出其他线程。同时,这个线程的主要的作用是维护一个vm操作队列(VMOperationQueue),用于处理其他线程提交的vm operation,比如执行GC等。
VmThread在执行一个vm操作时,先判断这个操作是否需要在safepoint下执行。若需要safepoint下执行且当前系统不在safepoint下,则调用SafepointSynchronize的方法驱使所有线程进入safepoint中,再执行vm操作。执行完后再唤醒所有线程。若此操作不需要在safepoint下,或者当前系统已经在safepoint下,则可以直接执行该操作了。所以,在safepoint的vm操作下,只有vm线程可以执行具体的逻辑,其他线程都要进入safepoint下并被挂起,直到完成此次操作。
因此,在执行字节码替换的时候需要在safepoint下执行,因此整体会触发stop-the-world。
运行时转换流程
- 对jvm方法区中类定义进行替换,因为堆(heap)中的Class对象是对方法区对象的封装,所以可以理解为对Class对象的替换。
对于一个对象方法的调用,可以理解为不改变对象头中的指向类的指针本身,而是只改变了内容。当一个class被替换后,系统无需重启,替换的类会立即生效
【JVM】字节码与ASM字节码增强、Instrument实现类的动态重加载相关推荐
- postgresql源码学习(57)—— pg中的四种动态库加载方法
一. 基础知识 1. 什么是库 库其实就是一些通用代码,可以在程序中重复使用,比如一些数学函数,可以不需要自己编写,直接调用相关函数即可实现,避免重复造轮子. 在linux中,支持两种类型的库: 1. ...
- 【web前端特效源码】使用HTML5+CSS3制作一个会动的音频loading加载动画效果~~适合初学者~超简单~ |前端开发|IT编程
b站视频演示效果: [web前端特效源码]使用HTML5+CSS3制作一个会动的音频loading加载动画效果~~适合初学者~超简单~ |前端开发|IT软件 效果图: 完整代码: <!DOCTY ...
- Linux动态库加载函数dlopen源码梳理(一)
下载了libc的源码,现在就开始libc源码的学习,最近了解到了linux动态库的相关知识,那么就从linux动态库加载函数dlopen进行梳理学习吧. 如果还没下载libc源码,可通过 https: ...
- JVM SandBox源码解析(一):启动时初始化、启动时加载模块、ModuleHttpServlet进行Http路由
前言 上篇JVM SandBox实现原理详解文章中,主要解析了JVM SandBox的核心实现原理,并且对SandBoxClassLoader和ModuleClassLoader做了源码解析,也解释了 ...
- 从源码来理解slf4j的绑定,以及logback对配置文件的加载
1)https://www.cnblogs.com/youzhibing/p/6849843.html 编译期间,完成slf4j的绑定已经logback配置文件的加载.slf4j会在classpath ...
- 查看开源操作系统ReactOS源码,解决dll库动态库加载失败问题(调用LoadLibrary加载失败)
目录 1.动态加载dll库去调用库中的函数 1.1.调用系统dll库中未公开的接口
- ASM字节码编程 | 用字节码增强技术给所有方法加上TryCatch捕获异常并输出
作者:小傅哥 博客:https://bugstack.cn Wiki:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有所收 ...
- API性能监控 【ApiHelp】-- 组件Enhance 代码实现 ~ ASM字节码增强
上篇文章主要介绍了Enhance组件的核心功能和设计思路,现在就来具体进行代码分析和实现. 主要知识点:java agent.字节码.ASM框架.Instrument 后续单独出文章介绍工具中所有使用 ...
- JVM中篇:字节码与类的加载篇
0.概述 0.1字节码文件的跨平台性 0.1.1.Java语言:跨平台的语言(write once,run anywhere) 当]ava源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再 ...
最新文章
- 收藏 | 使用Mask-RCNN在实例分割应用中克服过拟合
- 图像的均值和方差python_python-绘制均值和标准差
- Eric Lippert对C#的评论和展望
- 【微信小程序】mysql主从复制原理
- linux mint 18.3 内核,Linux Mint Linux用户可以升级到18.2 18.3”
- 技术实践 | Web 端实现 RTC 视频特效的解决方案
- c++hello world代码_在 Rust 代码中编写 Python 是种怎样的体验?
- win10系统下SQL2012下载及安装
- 一个嘉奖真心做事认真做事的时代
- Android自动化测试应用:uiautomatorviewer工具的安装与使用
- 2022计算机保研心得
- C语言指针中P、*P、P、**P的区别
- 【前端基础】20.JQuery基本语法
- 阿德莱德大学计算机专业教学,澳洲阿德莱德大学计算机硕士课程的专业设置如何?...
- 安装linux双系统简书,安装win10+ubuntu18.04双系统
- SDH原理--3.开销和指针
- 达闼科技赵开勇:基于自学习的机器人决策系统
- html无序列表项目符号移动,关于html:我需要一个没有任何项目符号的无序列表...
- 数据架构师是什么?来看这本书中的介绍
- 根据子网掩码计算主机数
热门文章
- 注意: 如何解决Windows Server 2008 R2 EFI启动模式安装2019年8月更新KB4512486 KB4512506 KB4512514后自动进入修复模式,无法正常启动问题!!!
- Linux 压缩解压和软件安装
- [二分查找] [luoguP3500] [POI2010] TES-Intelligence Test
- 【MySQL】数据库中的三大范式
- 关于Allan方差分析陀螺仪误差的几个摘要
- 训练fater rcnn时出现path not exist问题
- 老毛子(华硕)固件ipv6及dmz主机设置
- java嫦娥_嫦娥回来了,还有哪些浪漫传说已经实现?
- java程序员 女装_java程序员面试着装要求是什么?
- Luatos学习:Air101点灯