字节码篇
字数: 0 字 时长: 0 分钟
从 《JVM 架构图》 可以看到一切的起点都是从字节码文件开始的。
字节码文件 .class
文件是 JVM 的核心基石之一,《Java 虚拟机规范》中严格定义了 .class
文件的结构和指令集。
任何符合规范的 JVM(无论运行在什么操作系统或硬件上)都能理解并执行同一个 .class
文件,这就是 一次编译,到处运行
的根源。
java 编译器
java 是半编译半解释型语言,分为前端编译器与后端编译器
默认情况下,idea 使用前端编译器 javac
将 .java
源代码编译为 .class
字节码文件
前端编译器并不会涉及编译优化等方面的技术,而是将这些具体优化细节移交给 HotSpot 的 JIT 编译器
负责。
class 文件结构
.class
文件是一个高度结构化的二进制流,其结构由规范精确定义。
1. 魔数
文件头部的 4
个字节(CA FE BA BE
) :JVM 识别 .class
文件的唯一标志
2. 次版本号/主版本号
后续 4
个字节(各 2 字节) :指定 .class
文件格式的版本,JVM 会使用此信息检查兼容性,低版本 JVM 无法运行通过高版本编译器编译的字节码
3. 常量池
常量池 ConstantPool
是 .class
文件中最大和最关键的部分。它是表项的集合,用于存放编译时期生成的各种字面量和符号引用
常量池中的项来解释类名、方法名、字段名和常量值。不同类型的常量项有自己的格式标识。
字面量和符号引用
Java 代码在进行 javac
编译的时候,并不像 C/C++
那样有连接这一步骤,而是在虚拟机加载 .class
文件的时候进行动态链接。
也就是说,在 .class
文件中,不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址。
当虚拟机运行时,需要从常量池中获取对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
4. 访问标识 Access Flags
占用 2 个字节 :描述类或接口的访问权限和属性,如 public
、 private
、abstract
、interface
等
5. 当前类 This Class
2 个字节的索引,指向常量池中的 CONSTANT_Class_info
项,表示该类自身的类名
6. 父类 Super Class
2 个字节的索引,指向常量池中的一个 CONSTANT_Class_info
项,表示其直接父类。对于 java.lang.Object
,此值为 0
7. 接口 Interface
- 接口计数器(2字节) : 实现的接口数量。
- 接口列表 :后续数组,每个元素是 2 字节索引,指向常量池中的
CONSTANT_Class_info
项,表示该类所实现的接口。
8. 字段表 Fields
- 字段计数器(2字节) :字段数量。
- 字段表 :
field_info
结构数组,每个结构描述一个字段
9. 方法表 Methods
- 方法计数器(2字节) :方法数量。
- 方法表 :
method_info
结构数组,每个结构描述一个方法,包含访问标志、名称索引、描述符索引、属性表
10. 类属性表 Attributes
- 类属性计数器(2字节) :属性数量。
- 属性表 :存储应用于类级别的额外信息,如
SourceFile
源文件名、InnerClasses
内部类信息、BootstrapMethods
(支持invokedynamic
指令动态调用,是 Lambda/方法引用的实现关键) 等
窥探字节码工具
- JDK 提供了
javap
命令可以将.class
文件的二进制格式反编译为人类相对可读的格式
// 编译 源文件
javac Simple.java
// 反编译
javap -c -verbose Simple.class
- 使用 idea 插件
JVM 与字节码的生命周期
- 编译与加载流程
- 运行阶段