Java 王者修炼手册【JVM篇 - 字节码】:从字节码到字节码增强技术修炼

139 阅读12分钟

大家好,我是程序员强子。

又来刷英雄熟练度咯~今天专攻Jvm字节码~

2001116.jpg

我们来看一下,今晚我们准备练习哪些内容:

  • 字节码:本质,作用,JIT,文件结构,调用指令等
  • 字节码的增强技术: 基础概念,工具,与反射的区别及应用,存在的风险 等
  • Arthas 是怎么用代码增强的: 作用,原理

发车啦,系好安全带~

字节码

字节码的本质与作用

字节码是什么?

字节码是javac编译器将Java 源代码编译后的中间代码,以二进制形式存储在class文件

JVM 的核心工作就是解析和执行字节码

为什么能 “一次编写,到处运行”?

这是字节码的核心价值

不同操作系统,CPU指令集不同,但是只要安装了对应的jvm,同一份字节码就能翻译成本地机器码执行~

如果是在win编译成class文件,然后转移到mac,能运行吗?

能识别能运行!

首先,字节码不同平台下是 统一的,并不会因为是linux或者 mac 而不同标准(语法一致~类似全国有不同方言,但是普通话就是标准的)

传统编程语言(比如 C/C++)的源码编译后直接生成机器码,而Java 是把源码(.java)先被编译成字节码(.class)

不同平台会安装对应的 JVM,把统一的字节码(.class) 翻译成当前平台能识别的机器码

虽然字节码跨平台,但有个前提:编译字节码的 JDK 版本,不能高于运行它的 JVM 版本

那 win 从 maven下载的jar 包,转移到 mac 能正常识别吗?

几乎 100% 能正常识别和运行,核心原因和 “class 文件跨平台” 的逻辑一致

JAR 包的本质是 字节码 + 资源文件的压缩包

存在以下情况不能识别

  • JAR 包包含平台相关的本地代码(JNI)
  • 配置文件硬编码平台路径

但这些是本身编写程序的问题,跟标准无关~~

JVM 如何执行字节码?

JVM 采用 “解释执行 + 即时编译(JIT)” 混合模式:

  • 解释执行:逐条翻译字节码为机器码并执行,启动快(适合短命令行工具)
  • JIT 编译:对频繁执行的 热点代码(如循环、高频方法),在运行时编译为机器码并缓存,后续直接执行机器码

为啥我们需要JIT呢?我们来仔细研究一下~~

JIT

什么是JIT编译?

黑科技!

JIT 编译(Just-In-Time Compilation,即时编译)

能把频繁执行的 Java 字节码 **偷偷 **编译成机器码。

HotSpot 虚拟机里有两种 JIT 编译器:

  • C1 编译器(客户端编译器) :编译快,但优化少(比如只做简单的循环展开),适合桌面应用(追求启动快);
  • C2 编译器(服务端编译器) :编译慢,但优化狠(比如动态去重、逃逸分析),适合服务器应用(追求长期运行效率)。

现在的 JVM(比如 JDK8 及以上)默认用 分层编译

先让 C1 快速编译热点代码,保证初步提速

再让 C2 慢慢优化,生成更高效的机器码,进一步提升性能。

为什么需要JIT?

你要读一本英文书(类比 Java 字节码),有两种方式:

  • 逐句翻译(类比解释执行):读一句,查一句字典,刚开始快(不用提前准备),但读多了就很慢(重复查同一个词);
  • 提前标重点(类比 JIT 编译):读了几页后,发现某些句子(比如高频出现的专业术语)反复出现,就提前把这些句子翻译成中文记在旁边,后面再读到直接看翻译,速度翻倍。

怎么判断 “热点代码”?

热点探测器 ~

  • 方法调用计数器: 一个方法被调用的次数超过阈值(默认 1 万次左右),就被标记为热点;
  • 循环回边计数器:循环体执行的次数超过阈值(比如 for 循环跑了 10 万次),循环内的代码会被标记为热点。

一旦被标记为热点,JIT 编译器就会插队 把这段字节码编译成机器码,同时做很多优化

比如去掉无效判断、合并重复计算

字节码文件结构

严格按照 JVM 规范组织的二进制文件,核心结构包括:

  • 魔数(0xCAFEBABE):开头 4 字节,标识这是一个.class 文件(类似 “文件身份证”)。
  • 版本号:如52.0对应 Java 8,JVM 会拒绝执行版本不兼容的字节码。
  • 常量池:字节码的 字典,存储类名、方法名、常量值等(占文件大部分空间)。
  • 访问标志:标识类的类型(如public final class、interface)。
  • 类 / 父类 / 接口信息:记录继承关系(如java/lang/Object是所有类的父类)。
  • 字段表:类的成员变量信息(名称、类型、修饰符,如private String name)。
  • 方法表:方法的字节码指令、异常表等(核心执行逻辑)
  • 附加属性表: 源文件名称,注解信息等

如何查看字节码?

javap -c:生成字节码指令的反汇编代码

javap -v:输出详细(verbose)信息

比如一个简单的Test 类

public class Test {
    public int add(int a, int b) {
        return a + b;
    }
}

编译后执行 javap -c Test.class,输出如下(只输出关键部分):

public int add(int, int);
  Code:
     0: iload_1       // 加载第一个参数a到操作数栈
     1: iload_2       // 加载第二个参数b到操作数栈
     2: iadd          // 执行int类型加法
     3: ireturn       // 返回结果

-v 是 --verbose 的缩写,作用是输出类的全部详细信息,包含 -c 的所有内容,还额外展示:

  • 类的访问标志(如 public final);
  • 常量池(Constant Pool,存储字符串、类名、方法名等常量);
  • 字段和方法的完整属性(访问修饰符、描述符、属性表如 LineNumberTable、LocalVariableTable 等);
  • 类的继承关系、接口实现等元数据。

示例(截取部分):

执行 javap -v Test.class,会看到:

Classfile /Users/xxx/Test.class
  Last modified 2025-11-19; size 300 bytes
  MD5 checksum xxxxxxxx
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52  // JDK 8
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // Test
  super_class: #3                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class             #13            // Test
   #3 = Class             #14            // java/lang/Object
   // ... 更多常量池项
{
  public Test();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LTest;

  public int add(int, int);
    descriptor: (II)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 4: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   LTest;
            0       4     1     a   I
            0       4     2     b   I
}

字节码方法调用指令

Java 的方法调用在字节码层面通过不同指令实现,核心区别在于绑定时机:是在编译期确定还是运行时确定

配图1

学习字节码方法调用指令的作用是什么?

本质是穿透 Java 语法糖,理解 JVM 执行方法调用的底层逻辑

很多 Java 语法(如多态、构造方法、静态方法调用)的底层实现

都依赖不同的方法调用指令,学习它们能搞懂 “语法为什么是这样”

多态的实现原理

invokevirtual 指令会触发动态分派(根据对象实际类型查找方法),这就是子类重写方法时 “运行时才确定调用哪个方法” 的原因

invokestatic(静态方法)、invokespecial(私有 / 构造方法)属于静态分派(编译期确定目标方法),无法实现多态

// 父类
class Animal {
    public void say() {
        System.out.println("Animal say");
    }

    public static void staticMethod() {
        System.out.println("Animal static");
    }
}

// 子类
class Cat extends Animal {
    @Override
    public void say() {
        System.out.println("Cat say");
    }

    public static void staticMethod() {
        System.out.println("Cat static");
    }
}

// 测试类
public class PolymorphismDemo {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.say();          // 多态:输出 Cat say
        animal.staticMethod(); // 静态方法无多态:输出 Animal static
    }
}

字节码分析:

public static void main(java.lang.String[]);
  Code:
     0new           #2                  // class Cat(创建Cat实例)
     3: dup
     4: invokespecial #3                  // Method Cat."<init>":()V(调用Cat构造方法)
     7: astore_1                          // 赋值给Animal类型引用animal
     8: aload_1                           // 加载animal引用
     9: invokevirtual #4                  // Method Animal.say:()V(关键:invokevirtual动态分派)
    12: aload_1
    13: invokestatic  #5                  // Method Animal.staticMethod:()V(静态分派,编译期绑定Animal)
    16return
  • animal.say() 用 invokevirtual,JVM 会根据 animal 实际指向的 Cat 实例,调用 Cat 的 say();
  • animal.staticMethod() 用 invokestatic,编译期已绑定到引用类型 Animal 的静态方法,无多态。

构造方法的执行逻辑

  • 构造方法的字节码名为 ,由 invokespecial 指令调用;
  • 子类构造方法会隐式 / 显式调用父类构造方法(super),确保父类成员先初始化;
  • 若构造方法中调用重写方法,会触发多态(因为对象已部分初始化)。

案例

class Parent {
    public Parent() {
        System.out.println("Parent构造");
        this.method(); // 构造中调用方法
    }
    
    public void method() {
        System.out.println("Parent method");
    }
}

class Child extends Parent {
    private String name;
    
    public Child() {
        // 隐式调用super()(父类构造)
        name = "Child";
        System.out.println("Child构造");
    }
    
    @Override
    public void method() {
        System.out.println("Child method: " + name); // name此时未初始化!
    }
}

public class ConstructorDemo {
    public static void main(String[] args) {
        new Child();
    }
}

输出结果:

Parent构造
Child method: null // 子类method被调用,但name未初始化
Child构造

字节码分析:

public Child();
  Code:
     0: aload_0
     1: invokespecial #1                  // Method Parent."<init>":()V(先调用父类构造)
     4: aload_0
     5: ldc           #2                  // String Child
     7: putfield      #3                  // Field name:Ljava/lang/String;(初始化name)
    10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
    13: ldc           #5                  // String Child构造
    15: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    18: return
  • 子类构造第一行默认用 invokespecial 调用父类 ,父类构造执行完才会初始化子类成员;
  • 父类构造中调用的 method() 实际是子类重写版本(invokevirtual 动态分派),但此时子类成员未初始化,所以输出name 为空

字节码增强技术

基础概念

什么是字节码增强技术?

就是在编译期、类加载期或运行时修改 Java 字节码

动态添加 / 修改类的方法、字段或逻辑的技术

它绕过了源代码编译的限制,直接操作 JVM 能识别的字节码指令

常见工具对比

配图2

CGLIB demo:

// CGLIB示例
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    System.out.println("保存用户"); // 增强逻辑
    return proxy.invokeSuper(obj, args); // 执行原方法
});
UserService proxy = (UserService) enhancer.create();
proxy.save(new User());

与反射的区别

特性字节码增强反射
时机编译期 / 类加载期修改字节码运行时动态调用方法 / 字段
性能增强后代码与原生代码性能一致每次调用需解析类结构,性能损耗大
灵活性可修改类结构(加方法 / 字段)仅能调用已有方法 / 字段

典型应用场景

  • Spring AOP:对目标类生成 CGLIB 子类(或 JDK 动态代理),在子类中重写方法,插入事务、日志等增强逻辑。
  • 热部署工具(JRebel):类加载期替换旧的.class 字节码,无需重启 JVM 即可生效(本质是自定义类加载器 + 字节码替换)。
  • 监控工具(SkyWalking):通过 ByteBuddy 在运行时给方法加埋点, 统计调用耗时、异常信息
  • ORM 框架(Hibernate):用 CGLIB 生成实体类代理,实现延迟加载

Arthas 是怎么用代码增强的

Arthas 是什么?有什么用?

Arthas 是阿里巴巴开源的 Java 诊断工具,堪称 Java 开发者的 线上问题排查神器

它能在不重启应用的前提下,实时监控、调试、诊断运行中的 Java 程序

核心功能

解决开发者 线上日志不够本地复现不了 的困境

  • watch:监控方法的入参、返回值、异常,相当于给方法加 日志探针
  • trace:追踪方法调用链路耗时,相当于给方法调用链加 埋点
  • redefine热更新类代码,直接替换运行中的类字节码;
  • monitor:统计方法调用次数成功率,相当于给方法加 计数器

Arthas 代码增强原理

Attach API 和 ASM 框架

Attach API 是什么?

Attach API 是 JVM 提供的一套进程附着机制,简单说就是:允许一个 Java 进程(比如 Arthas、VisualVM)主动连接到另一个正在运行的 Java 进程(比如你的业务应用),并对目标进程的 JVM 进行操作。

Arthas 启动时,会先扫描本机所有运行的 Java 进程(用jps命令也能看到)

选择要诊断的进程 ID 后,Arthas 就通过 Attach API 附着 到这个进程上

这是 Arthas 能操作目标 JVM 的第一步

当外部进程通过 Attach API 连接到目标 JVM 后,会拿到一个关键工具 Instrumentation接口的实例

这个实例堪称 JVM 的 字节码手术刀,是实现运行时代码增强、类重定义的核心

Instrumentation接口的实例有什么作用?

  • 修改已加载类的字节码:比如 Arthas 用它把监控逻辑(如watch命令的埋点)插入到目标方法的字节码中;
  • 重新定义类:直接替换运行中的类(Arthas 的redefine命令就是靠它实现热更新);
  • 添加类转换器:注册一个 “转换器”,JVM 加载新类时会自动用转换器修改字节码(比如全局监控所有新加载的 Controller 类);
  • 获取 JVM 加载的所有类:遍历目标 JVM 中已加载的类,方便定位要增强的类。

简单的说,外部工具拿到这个实例,就能对目标 JVM 的类为所欲为

Instrumentation 实例 跟 ASM框架 有什么关联?

  1. 通过 Attach API 拿到Instrumentation实例,即获取 修改权限
  2. 用 ASM 编写 字节码修改逻辑, 比如给UserService.getUser方法插入入参记录的指令;
  3. 调用Instrumentation的方法(如redefineClasses()),把 ASM 改好的字节码 提交 给 JVM;
  4. JVM 用新的字节码替换掉原来的类,完成增强

总结

今天把字节码相关的底层地基给学扎实了!

字节码的本质、JIT 编译的门道、.class 文件结构,还有 invokevirtual 这些调用指令的套路,全扒得透透的~

字节码增强的概念ASM 和 CGLIB 的区别,还有它跟反射的性能差异、实际应用场景,连 Arthas 用代码增强搞监控调试的原理,也都捋明白了!

下一场该啃 JVM 底层原理的硬骨头了!方法区的分工、对象在新生代到老年代的流转、还有 OOM 排查的实战技巧。。。

这些可是调优排障的核心本事,必须吃透原理,练出实操手感。

熟练度刷不停,知识点吃透稳,下期接着练~