Skip to content

字节码与多语言混合编程

字节码:JVM 的「机器语言」

前面我们说 JVM 不关心源代码是什么语言,只关心字节码。字节码究竟是什么?

简单来说,字节码是一组有序的字节序列,每条指令是一个字节(0~255),这就是「字节码」名字的由来。

看一个真实的例子

写一段最简单的 Java 代码:

java
public class Demo {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = a + b;
        System.out.println(c);
    }
}

javac 编译后,用 javap -c 查看字节码:

java
public static void main(java.lang.String[]);
    Code:
      iconst_10          // 常量 10 入栈
      istore_1           // 弹出栈顶,存入局部变量槽 1(a)
      bipush 20          // 将 20 入栈
      istore_2           // 弹出栈顶,存入局部变量槽 2(b)
      iload_1            // 局部变量槽 1 的值(10)入栈
      iload_2            // 局部变量槽 2 的值(20)入栈
      iadd               // 弹出两个栈顶整数相加,结果入栈
      istore_3           // 结果存入局部变量槽 3(c)
      getstatic #2       // 获取 System.out 静态字段
      iload_3            // c 的值入栈
      invokevirtual #3   // 调用 println 方法
      return             // 方法返回

每一条 iconst_istore_iadd 都是一个字节的 opcode(操作码),后面可能跟着 0~2 个字节的操作数。这就是 JVM 的指令集。

Class 文件结构一览

字节码不只是方法的指令,还包含了整个类的元信息。Class 文件的基本结构:

ClassFile {
    u4 magic;               // 魔数:0xCAFEBABE(JVM 识别文件就靠它)
    u2 minor_version;        // 次版本号
    u2 major_version;        // 主版本号(52 = JDK 8, 61 = JDK 17)
    u2 constant_pool_count;  // 常量池中条目数量
    cp_info constant_pool[constant_pool_count-1];  // 常量池
    u2 access_flags;         // 访问标志(public/private/final...)
    u2 this_class;           // 当前类索引
    u2 super_class;           // 父类索引
    u2 interfaces_count;     // 接口数量
    u2 interfaces[interfaces_count];
    u2 fields_count;         // 字段数量
    field_info fields[fields_count];
    u2 methods_count;        // 方法数量
    method_info methods[methods_count];
    u2 attributes_count;     // 属性数量
    attribute_info attributes[attributes_count];
}

这个结构是固定的。无论用 Java、Kotlin 还是 Scala 编译,最终都生成这个格式的 Class 文件。这也是为什么不同语言能互操作的根本原因——它们说的是同一种「字节码语言」。

多语言混合编程:Kotlin 和 Java 共存

最典型的多语言混合编程场景是:同一个项目中,Java 和 Kotlin 代码共存

Spring、Kafka 等大型项目都这样用。这种场景之所以能实现,依赖三个基础:

1. 统一的字节码格式

Java 和 Kotlin 编译器都输出标准 Class 文件。一个 Java 类可以继承一个 Kotlin 类,可以实现 Kotlin 接口,可以调用 Kotlin 方法——因为在字节码层面,它们是同一回事。

2. 统一的类型系统

JVM 的类型系统是统一的:

  • 基本类型:intlongboolean……
  • 引用类型:StringObject、各种自定义类……

Kotlin 的 Int 在编译后就是 JVM 的 intString?(可空)在字节码里就是普通的 String,可空性信息编译后被擦除。这样 Java 代码看到的 Kotlin 类型,和 Java 原生的类型完全兼容。

3. 统一的名称修饰(Name Mangling)

Java 和 Kotlin 都有重载,但 Kotlin 还有默认参数。比如:

kotlin
class Greeter {
    fun greet(name: String, times: Int = 1) {
        // ...
    }
}

Java 没有默认参数,所以 Java 调用 greet() 时如果只传一个参数,Kotlin 编译器会自动生成一个桥接方法(bridge method):

java
// Java 看到的实际方法签名
public void greet(String name, int times);

// Kotlin 编译器额外生成的桥接方法
public void greet(String name) {
    greet(name, 1);
}

这个桥接过程发生在编译期,JVM 看到的就是两个普通方法,没有任何特殊性。

混合编程的实战场景

场景一:逐步迁移

公司有个老的 Java 项目,想迁移到 Kotlin。不需要一次性全部重写,可以逐步替换——Java 调用 Kotlin、Kotlin 调用 Java,就像同一个语言一样。

场景二:取长补短

  • 用 Java 写核心业务逻辑(稳定、性能)
  • 用 Kotlin 写 DSL 和脚本(简洁、表达力强)
  • 用 Groovy 写测试用例(动态、灵活)

场景三:大数据生态

Scala 是大数据领域的标配语言(Hadoop、Spark、Kafka 核心都是 Scala),但业务代码通常用 Java。大数据框架暴露的 API 是统一的字节码接口,Java 和 Scala 代码可以无缝混用。

字节码不是银弹:混合编程的坑

虽然字节码层面是统一的,但不同语言在编译后的行为可能不同,带来一些需要注意的问题:

问题说明
空指针Kotlin 有空安全机制,但和 Java 互调时,Java 传过来的值 Kotlin 无法保证非空
泛型擦除Kotlin 的泛型在字节码中和 Java 一样会被擦除,协变/逆变只存在于编译期
同步机制Kotlin 没有 synchronized 关键字,编译器会自动生成同步方法体,和 Java 的 synchronized 行为一致
反射Java 反射可以读取 Kotlin 的 metadata 属性(如 isXxx getter),但要注意 Kotlin 编译器版本

动手看看字节码

用 javap 工具可以查看任意 .class 文件的字节码:

bash
# 查看反汇编(指令)
javap -c MyClass.class

# 查看详细信息(常量池、变量名等)
javap -v MyClass.class

# 查看行号表(用于调试)
javap -g MyClass.class

# 查看 public/private 方法的签名
javap -p MyClass.class

如果想看更详细的字节码分析,可以使用 JBE(Java Bytecode Editor)ASM Framework 进行字节码级别的操作。

本节小结

字节码是 JVM 的「通用语言」:

  • 它是一种固定的二进制格式(Class 文件)
  • 所有能编译成这个格式的语言,都能跑在 JVM 上
  • 这就是 Java、Kotlin、Scala 等语言能无缝互调的根本原因

混合编程的好处是各取所长,但也要注意语言特性在编译后可能带来的差异。

下一节,我们来回顾一下 Java & JVM 历史重大事件,了解这项技术是怎么一步步走到今天的。

基于 VitePress 构建