从 Java 源码到字节码
Java 源码经过编译后成为 Class 文件。Class 文件记录类型、字段、方法和 JVM 指令,不绑定某一种处理器;JVM 在加载并验证它之后,才通过解释器或即时编译器执行其中的方法。
1. javac 输出的是 Class 文件
public final class Price {
public static int total(int unitPrice, int quantity) {
return unitPrice * quantity;
}
}
javac Price.java
javap -c -p Price.class
javac 先解析源码并完成类型检查,再把合法程序转换为 Class 文件。编译器会处理泛型擦除、语法糖展开和部分常量折叠,但不会把普通 Java 方法直接编译成当前机器的原生指令。
同一个 .java 文件可能产生多个 Class 文件,例如成员类、匿名类和某些编译器生成的辅助类型。文件数量不是判断源码结构的可靠依据。
2. Class 文件保存了什么
Class 文件主要包含:
- 魔数与 Class 文件版本。
- 当前类型、父类和接口信息。
- 常量池。
- 字段、方法及其属性。
- 方法的字节码、异常表、行号表和栈映射帧等属性。
常量池保存字符串常量,也保存类、字段和方法的符号引用。符号引用描述“要找谁”,类加载后的解析过程才可能把它转换为能够直接定位运行时目标的引用。
javap -v Price.class
-v 可以看到常量池、方法描述符、访问标志和属性,适合判断编译器实际生成了什么。
3. JVM 指令使用局部变量表和操作数栈
total 的核心指令可以简化为:
iload_0
iload_1
imul
ireturn
两个参数先压入操作数栈,imul 弹出两个整数并压入乘积,ireturn 返回结果。每次方法调用对应一个栈帧,栈帧包含局部变量表、操作数栈以及完成调用所需的信息。
字节码是 JVM 指令集,不等于某种 CPU 汇编。HotSpot 可以解释这些指令,也可以把热点方法编译为机器码。
4. 方法描述符保留 JVM 需要的类型
源码中的方法:
List<String> find(long userId)
其描述符只记录擦除后的参数和返回类型:
(J)Ljava/util/List;
泛型信息通常保存在 Signature 属性中,JVM 方法调用仍按擦除后的描述符匹配。桥接方法、装箱、增强 for 等源码概念,也要以最终 Class 文件为准。
5. 字节码验证保护运行时边界
Class 文件进入 JVM 后还要验证。验证会检查文件格式、类型约束、指令操作数和控制流等内容,避免非法字节码破坏 JVM 的运行时状态。
javac 能生成合法字节码,不代表 JVM 只能运行 javac 的产物。Kotlin、Scala 等语言也可以生成符合 Class 文件规范的字节码;字节码工具还可以在编译后修改类。因此验证是类加载的一部分,不能只依赖源码编译器。
6. 从字节码到执行结果
一段方法通常经历:
- 类加载器找到 Class 文件并定义运行时类型。
- JVM 完成验证、准备、必要的解析和初始化。
- 解释器开始执行方法并收集运行时画像。
- 热点方法可能由 JIT 编译为机器码。
- 优化假设失效时,已编译代码可能去优化并回到较低层级执行。
因此,“Java 是编译型语言还是解释型语言”没有单一二选一答案。源码先编译为字节码,运行时又可能同时使用解释执行与即时编译。
7. 常见问题
7.1 javac 和 JIT 都叫编译,有什么区别
javac 在程序运行前把源码转换为 Class 文件;JIT 在程序运行期间根据实际调用与类型画像,把热点字节码转换为当前平台的机器码。前者提供可移植产物,后者利用运行时信息优化热点路径。
7.2 反编译结果为什么和源码不同
编译器会擦除泛型、生成桥接方法、插入装箱调用并展开语法糖。反编译器根据 Class 文件重建近似源码,无法保证恢复原来的变量名、结构和书写方式。
7.3 字节码相同,运行速度就相同吗
不一定。JDK 版本、JVM 实现、运行参数、实际输入画像和硬件都会影响解释、编译、内联和垃圾回收结果。
8. 面试题
8.1 Java 程序从源码到运行经历了哪些过程
出现公司:去哪儿、字节跳动
考察重点
javac、Class 文件、类加载与运行时执行的边界。- 字节码和机器码的区别。
- 解释器与 JIT 为什么可以同时存在。
相关内容:第 1 节“javac 输出的是 Class 文件”至第 6 节“从字节码到执行结果”。
参考回答
javac 先完成语法和类型检查,把源码编译为平台无关的 Class 文件。类加载器找到字节流后,JVM 创建运行时类型并完成验证、准备、解析和初始化。方法开始时可以由解释器执行,热点路径再由 JIT 编译为当前平台机器码;优化假设失效时还可能去优化。
回答时要把编译期、类加载期和运行期分开。Class 文件是 JVM 的输入,不是 CPU 可以直接执行的机器码。
8.2 怎样确认一段 Java 语法最终生成了什么
出现公司:京东数科
考察重点
- 使用
javap检查 Class 文件。 - 区分源码推断和运行时证据。
- 泛型擦除、桥接方法和调用指令。
相关内容:第 2 节“Class 文件保存了什么”至第 4 节“方法描述符保留 JVM 需要的类型”。
参考回答
先用同一 JDK 和同一编译参数执行 javac,再用 javap -v -c -p 查看常量池、描述符、属性和指令。若还要确认运行时目标,需要结合类加载日志、JIT 日志或 JFR,因为字节码只说明编译产物,不足以证明最终是否内联或编译。