添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

什么是字节码?

  • Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用
  • 之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即 以字节为单位进行读取
    在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图所示。
    在这里插入图片描述
  1. 编译:由javac编译,实现.java文件编译为.class文件(字节码文件)
  2. 加载:JVM加载,读取.class文件,并且由classLoader加载
  3. 校验:bytecode verifier是JVM中对字节码进行校验的部分
  4. 执行:由解释器执行;部分热点代码由JIT即时编译器编译为本地代码执行。

字节码结构

  • 常量池
    常量池中存储两类常量: 字面量与符号引用 。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区

操作数栈与字节码

  • JVM的 指令集是基于栈 而不是寄存器,基于栈可以具备很好的 跨平台性 (因为寄存器指令集往往和硬件挂钩),但缺点在于,要完成同样的操作,基于栈的实现需要更多指令才能完成(因为栈只是一个FILO结构,需要频繁压栈出栈)。另外,由于栈是在内存实现的,而寄存器是在CPU的高速缓存区,相较而言,基于栈的速度要慢很多,这也是为了跨平台性而做出的牺牲。

字节码增强

  • 字节码增强技术就是一类 对现有字节码进行修改 或者 动态生成全新字节码文件 的技术
  • 对于需要手动操纵字节码的需求,可以使用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的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。
  1. 首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。
public class Base {
    public void process(){
        System.out.println("process");
  1. 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);
    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    @Override
    public 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);
        @Override
        public 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);
        @Override
        public 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);
  1. 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!!!!!");
  1. 运行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实现热加载的过程

步骤有两个

  1. addTransformer(ClassFileTransformer transformer, boolean canRetransform)
    注册Transformer
  2. 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被替换后,系统无需重启,替换的类会立即生效
    在这里插入图片描述
字节码与ASM字节码增强以下内容摘自:字节码增强技术探索什么是字节码?Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图所示。编译:由javac编译,
字节码增强技术:AOP技术其实就是字节码增强技术,JVM提供的动态代理追根究底也是字节码增强技术。 目的:在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。 应用场景:某一天系统出现OOM,通过工具分析,各的对象占用了很大空间,但是这个对象被许多程序访问,那么就很难找到,工程的全文匹配也只能找到自己的业务代码调用的地方,深入的反射,三方包调用无法匹配。这个时候AOP就可以帮助完成。 两种实现机制:
JVMJava Virtual Machine)是Java程序的执行环境。当你运行一个Java程序时,它首先被编译成字节码,然后JVM字节码解释成机器码并执行。 JVM加载过程可以分为以下几个步骤: 1. 加载(Loading):加载指的是将.class文件读入内存,并为之创建一个java.lang.Class对象。加载器会负责从文件系统、JAR文件或网络中加载字节码数据。 2. 链接(Linking):链接分为三个阶段,分别是验证(Verification)、准备(Preparation)和解析(Resolution)。 * 验证:验证字节码是否符合JVM规范,并且不会危害JVM的安全。如果验证失败,则会抛出java.lang.VerifyError异常。 * 准备:为的静态变量分配内存,并将其初始化为默认值(0、null等)。 * 解析:将、接口、字段和方法的符号引用解析为实际引用。这个过程可能需要在运行时进行。 3. 初始化(Initialization):在加载过程中,初始化是最后一步。在这个阶段,静态变量被初始化,静态块被执行。如果初始化一个时发生异常,则会抛出java.lang.ExceptionInInitializerError异常。 JVM加载器有以下几种: 1. 启动加载器(Bootstrap ClassLoader):它是最顶层的加载器,负责加载JVM的核心库,如java.lang和java.util等。 2. 扩展加载器(Extension ClassLoader):它加载Java平台扩展库的。默认情况下,它从$JAVA_HOME/jre/lib/ext目录加载。 3. 系统加载器(System ClassLoader):也称应用程序加载器,它加载应用程序路径上的。 4. 用户自定义加载器:开发人员可以继承java.lang.ClassLoader,以实现自己的加载器。 总之,JVM加载过程是Java程序运行的要部分,它可以确保Java程序的正确执行。