我们所编写的各类语言的程序编译成二进制机器码(Native Code)已不再是唯一的选择,越来越多程序语言编译选择与操作系统、机器指令集无关的、平台中立的输出格式;
无关性基石
各种不同平台的 JVM,以及所有平台统一支持的编译结果存储格式(字节码,Byte Code)是构成平台无关性
和语言无关性
的基石;
语言无关性
,JVM 不与 Java 语言在内的任何程序语言绑定,只与Class 文件
的二进制字节码格式绑定,其中 Class 文件包含 JVM 指令集、符号表、其他辅助信息等;
Java 技术非常良好的向后兼容性得益于 Class 文件结构的稳定性;Class 文件结构基本只在原有基础上新增内容、扩充功能,而非在已定义的内容上做修改;
任意一个 Class 文件都对应着唯一的一个类或接口的定义信息(package-info.class、module-info.class 除外),反之则不一定(类或接口的动态生成,无需落到磁盘);其不像 XML 等描述语言,没有任何分隔符,严格限定了数据项的顺序和数量、数据存储的字节序等;
Class 文件格式只存在两种数据类型:
无符号数
,以 u1、u2、u4、u8 分别代表 1、2、4、8 个字节的无符号数,可以用来描述数字、索引引用、数量值、UTF-8 编码代表的字符等;表
,由多个无符号数或其他表构成的复合数据类型,表的命名习惯以_info
结尾;
Class 文件格式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_counts |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
1. 魔数
Class 文件的头 4 个字节即为魔数(Magic Number),其唯一作用是确定 Class 文件格式是否能被 JVM 接受(类似扩展名);Class 文件的魔数为 OxCAFEBABE
(咖啡宝贝);
2. Class 文件的版本
Class 文件中紧跟魔数的 4 个字节是 Class 文件的版本号,第 5、6 字节是 Minor Version(次版本号),第 7、8 字节是 Major Version(主版本号);
高版本 JDK 可以向下兼容旧版 Class 文件,但面对超过 JDK 版本的 Class 文件,即使文件格式未发生变化也是拒绝执行的;
Java 文件版本号
JDK 版本 | -target 参数 | -source 参数 | 版本号 |
---|---|---|---|
JDK 1.1.8 | 不支持 target 参数 | 不支持 source 参数 | 45.3 |
JDK 1.2.2 | 不带(默认为 -target 1.1) | 1.1 ~ 1.2 | 45.3 |
JDK 1.2.2 | -target 1.2 | 1.1 ~ 1.2 | 46.0 |
JDK 1.3.1_19 | 不带(默认为 -target 1.1) | 1.1 ~ 1.3 | 45.3 |
JDK 1.3.1_19 | -target 1.3 | 1.1 ~ 1.3 | 47.0 |
JDK 1.4.2_10 | 不带(默认为 -target 1.2) | 1.1 ~ 1.4 | 46.0 |
JDK 1.4.2_10 | -target 1.4 | 1.1 ~ 1.4 | 48.0 |
JDK 5.0_11 | 不带(默认为 -target 1.5) | 1.1 ~ 1.5 | 49.0 |
JDK 5.0_11 | -target 1.4 -source 1.4 | 1.1 ~ 1.5 | 48.0 |
JDK 6 | 不带(默认为 -target 6) | 1.1 ~ 6 | 50.0 |
JDK 7 | 不带(默认为 -target 7) | 1.1 ~ 7 | 51.0 |
JDK 8 | 不带(默认为 -target 8) | 1.1 ~ 8 | 52.0 |
JDK 9 | 不带(默认为 -target 9) | 6 ~ 9 | 53.0 |
JDK 10 | 不带(默认为 -target 10) | 6 ~ 10 | 54.0 |
JDK 11 | 不带(默认为 -target 11) | 6 ~ 11 | 55.0 |
JDK 12 | 不带(默认为 -target 12) | 6 ~ 12 | 56.0 |
JDK 13 | 不带(默认为 -target 13) | 6 ~ 13 | 57.0 |
Minor Version 只在 JDK 2 短暂使用过,直到 JDK 12 中间版本的 JDK 的 Minor Version 皆固定为 0;
JDK 12 开始,带有公测
特性的技术预览版本会将 Minor Version 置为 65535;
3. 常量池
Class 文件中紧接 Major Version 的是常量池;是 Class 文件结构中与其他项关联最多的数据,也是占用空间最大的数据项之一;
常量池中常量的数量不固定,因此常量池入口的一个 u2 型数据表示了常量池容量(constant_pool_count);该容量计数以 1 开始(不是 0,其他如接口索引集合、字段表集合、方发表集合等,是从 0 开始索引);
constant_pool_count 表示常量个数加 1,这个 1
表达不引用任何一个常量池项
,索引位占用了 0
;
常量池类别
字面量
(Literal),类似常量的概念,如字符串、final 的常量值等;符号引用
(Symbolic References),属于变异原理方面的概念;- 被模块导出或开放的包(Package);
- 类或接口的全限定名(Fully Qualified Name);
- 字段的名称和描述符(Descriptor);
- 方法的名称和描述符;
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic);
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant);
常量池中的每一项都是一个表,表的结构由第一个 u1 类型的标志位区分;截止 JDK 13,常量表共有 17 种不同类型,每个类型各有完全独立的数据结构;
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Flaot_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_Dynamic_info | 17 | 一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 一个动态方法调用点 |
CONSTANT_Module_info | 19 | 一个模块 |
CONSTANT_Package_info | 20 | 一个模块中开放或导出的包 |
通过 javap
解析如下代码编译后的 Class 文件;与 Class 文件的 Hex 格式对比(对照字节码数据类型翻译对比);
Java 源码
package edu.aurelius.jvm.clazz;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
Class 文件 Hex 格式
javap 解析结果
javap -verbose TestClass
Warning: Binary file TestClass contains edu.aurelius.jvm.clazz.TestClass
Classfile /jvm-best-practices/target/classes/edu/aurelius/jvm/clazz/TestClass.class
Last modified Feb 5, 2023; size 399 bytes
MD5 checksum 889e306f2098bb3e76eacac7fba77eef
Compiled from "TestClass.java"
public class edu.aurelius.jvm.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // edu/aurelius/jvm/clazz/TestClass.m:I
#3 = Class #20 // edu/aurelius/jvm/clazz/TestClass
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ledu/aurelius/jvm/clazz/TestClass;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 TestClass.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 edu/aurelius/jvm/clazz/TestClass
#21 = Utf8 java/lang/Object
{
public edu.aurelius.jvm.clazz.TestClass();
descriptor: ()V
flags: 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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ledu/aurelius/jvm/clazz/TestClass;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Ledu/aurelius/jvm/clazz/TestClass;
}
SourceFile: "TestClass.java"
4. 访问标志
Class 文件中紧跟常量池的是访问标志(access_flags),用于识别一些类或接口的访问信息(是类还是接口、是否 public、是否 abstract、是否 final 等);
标志含义对照表
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public 类型 |
ACC_FINAL | 0x0010 | 是否声明 final,仅限类 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义,JDK 1.0.2 之后编译的类必须为真 |
ACC_INTERFACE | 0x0200 | 是否接口 |
ACC_ABSTRACT | 0x0400 | 是否 abstract 类型,对接口和抽象类是真,其他为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非用户代码产生 |
ACC_ANNOTATION | 0x2000 | 是否注解 |
ACC_ENUM | 0x4000 | 是否枚举 |
ACC_MODULE | 0x8000 | 是否模块 |
5. 类索引、父类索引、接口索引集合
Class 文件紧接访问标志之后的是类索引(u2)、父类索引(u2)、接口索引集合(u2 集合);用于确定该类型的继承关系;
- 类索引确定这个类的全限定名;
- 父类索引确定这个类的父类的全限定名;除了
java.lang.Object
,所有 Java 类都有父类; - 接口索引集合描述该类实现了哪些接口;接口索引集合入口的第一个 u2 类型数据表示接口计数器(interfaces_count),即索引表的容量;
6. 字段表集合
字段表(field_info)用于描述接口或类中声明的变量(不包括定义在方法中的局部变量);
字段表结构
类型 | 名称 | 描述 | 数量 |
---|---|---|---|
u2 | access_flags | 字段修饰符,与类的 access_flags 类似 | 1 |
u2 | name_index | 字段的简单名称,如方法 inc() 和字段 m 的简单名称分别是 inc 和 m | 1 |
u2 | descriptor_index | 字段和方法的描述符,描述字段的类型、方法的参数列表(包括数量、类型、顺序)和返回值 | 1 |
u2 | attributes_count | 1 | |
u2 | attributes | 用于存储一些额外信息,如 Constant Value 属性描述字段为常量 | attributes_count |
字段访问标识
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public |
ACC_PRIVATE | 0x0002 | 是否 private |
ACC_PROTECTED | 0x0004 | 是否 protected |
ACC_STATIC | 0x0008 | 是否 static |
ACC_FINAL | 0x0010 | 是否 final |
ACC_VOLATILE | 0x0040 | 是否 volatile |
ACC_TRANSIENT | 0x0080 | 是否 transient |
ACC_SYNTHETIC | 0x1000 | 是否编译器自动生成 |
ACC_ENUM | 0x4000 | 是否枚举 |
descriptor
标识字符 | 含义 |
---|---|
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型,如 Ljava/lang/Ojbect; |
数组类型,每一维度将使用一个前置的 [
字符来描述,如 java.lang.String[][]
类型的二维数组呗记录成 [[Ljava/lang/String;
;
字段表集合不会列出父类或父接口的字段,但可能出现原 Java 代码中不存在的字段,如内部类为了保持对外部类的访问下,会自动添加纸箱外部类实例的字段;
7. 方法表集合
Class 文件存储的对方法的描述与对字段的描述几乎完全一致;方法表的结构与字段表的结构一样,依次是访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes),详情参照上文字段表结构描述;
方法访问标识相比字段访问标志,少了 volatile、transient、enum,多了 synchronized、native、strictfp、abstract;
方法访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public |
ACC_PRIVATE | 0x0002 | 是否 private |
ACC_PROTECTED | 0x0004 | 是否 protected |
ACC_STATIC | 0x0008 | 是否 static |
ACC_FINAL | 0x0010 | 是否 final |
ACC_SYNCHRONIZED | 0x0020 | 是否 synchronized |
ACC_BRIDGE | 0x0040 | 是否由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 是否 native |
ACC_ABSTRACT | 0x0400 | 是否 abstract |
ACC_STRICT | 0x0800 | 是否 strictfp |
ACC_SYNTHETIC | 0x1000 | 是否编译器自动生成 |
方法里面的代码经过 javac 编译后成字节码后,存放在方法属性集合中的名为 Code
的属性中;
在 Java 语言中重载(Overload)一个方法,需要与原方法具有相同的简单名称和特征签名(方法的各个参数在常量池中字段符号引用的集合),因此无法仅仅依靠返回值不同重载一个方法;但在 Class 文件格式中,只要方法的描述符不完全一致,是运行仅仅返回值不一的两个方法共存于同一 Class 文件的;
8. 属性表集合
字段表、方法表都可以携带自己的属性表集合,以描述某些场景的专有信息;
《Java 虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息;JVM 运行时会自动忽略不认识的属性;
属性表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
属性的名称是从常量池引用的一个 CONSTANT_Utf8_info 类型的常量表示的;
属性值的结构完全自定义;
虚拟机规范预定义的属性
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java 代码编译成的字节码指令 |
ConstantValue | 字段表 | 由 final 关键字定义的常量值,会通知 JVM 自动赋值 |
Deprecated | 类、方发表、字段表 | 被声明为 deprecated 的类、方法和字段 |
Exceptions | 方法表 | 方法抛出的异常列表,方法描述时 throws 关键后列举的受查异常 |
EnclosingMethod | 类文件 | 局部类或匿名类才拥有的属性,标示类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumnberTable | Code 属性 | Java 源码的行号与字节码指令的对应关系,与抛异常的行号提示、调试时的行断点设置有关 |
LocalVariableTable | Code 属性 | 方法的局部变量表描述,栈帧中变量与 Java 源码中变量的映射关系;与方法调用和调试时参数名称的显示有关 |
StackMapTable | Code 属性 | JDK 6 新增,检查目标方法的局部变量和操作数栈所需类型是否匹配(Type Checker) |
Signature | 类、方法表、字段表 | JDK 5 新增,用于支持泛型的方法前面,包含 Type Variables 或 Parameterized Types 的类、接口、初始化方法或成员的泛型前面会被 Signature 记录泛型信息,以应对 Java 的泛型擦拭法实现 |
SourceFile | 类文件 | 记录源文件名称,与抛异常时显示错误代码所在文件名有关 |
SourceDebugExtension | 类文件 | JDK 5 新增,存储额外的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
… |
Code 属性表结构
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | CONSTANT_Utf8_info 型常量的索引,代表该属性的名称,固定为 Code |
u4 | attribute_length | 1 | 属性长度 |
u2 | max_stack | 1 | 操作数栈深度的最大值 |
u2 | max_locals | 1 | 局部变量表所需的存储空间,单位为变量槽(slot),32 位,除了 long 和 double 需要两个 slot,其他数据类型都只占用 1 slot,非 static 方法自带一个 this 变量,变量槽永远大于 0 |
u4 | code_length | 1 | 字节码长度 |
u1 | code | code_length | 字节码指令的一些列字节流 |
u2 | exception_table_length | 1 | 异常处理表长度 |
exception_info | exception_table | exception_table_length | 显式异常处理表集合 |
u2 | attributes_count | 1 | |
attribute_info | attributes | attributes_count |
上一篇:「JVM 性能调优」应用程序启动耗时与延时优化
下一篇:「JVM 执行子系统」字节码指令简介
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》