Java之所以可以"一次编译,到处运行", 一是因为Jvm针对各种操作系统,平台都进行了定制。二是无论在什么平台,都可以编译生成固定格式字节码(.class)文件供JVM使用。
public class Test {
public int math() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Test test = new Test();
test.math();
}
}
编译成.class文件用UE编辑器打开
上面看似杂乱的十六进制字节码,其实是有一定的规范的。JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如下图。
1. 魔数
每个class文件的头4个字节称之为魔数(Megic Number)
CA FE BA BE
它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM 可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。
很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意改动。
2. 版本号
版本号为魔数之后的 4 个字节
00 00 00 34
前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。
3. 常量池
紧接着主版本号之后的字节为常量池入口。常量池中存储两种类型常量: 字面量和符号运用。
-
字面量(Literal):文本字符串(“ABC”)、基本数据类型的值(1,1.0)
-
符号引用(Symbolic References)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
可以将类信息理解成框架,常量池保存具体数据。因为无论是后面类名称或者是字段名、方法名都是保存在常量池,它们相应位置只保存在常量池中的偏移量。
常量池组成
常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图:
1)常量池计数器(constant_pool_count): 由于常量池的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。上面示例代码的常量池计数器为“0014”,转换为十进制后可以得到20,排除下标 0,也就是说这个类文件有 19 个常量。
2)常量池数据区:数据区是由(constant_pool_count - 1)个 cp_info 结构组成,一个 cp_info 的结构对应一个常量。在字节码中共有 14 种类型的 cp_info ,每种类型的结构都是固定的,如下表所示:
4. 访问标识
常量池结束之后的两个字节,描述该 Class 是类还是接口,以及是否被 Public、Abstract、Final 等修饰符修饰。JVM 规范规定了如下表所示的 8 种访问标志:
需要注意的是,JVM 并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为 public final,则对应的访问修饰符的值为 ACC_PUBLIC | ACC_FINAL,即 0x0001 | 0x0010 = 0x0011
5. 当前索引
访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
6. 父类索引
当前类名的后两个字节,描述父类的全限定名。这两个字节保存的值也是在常量池中的索引值,根据索引值就能在常量池中找到这个类的父类的全限定名。
7. 接口索引
父类名称后的两个字节,描述这个类的接口计数器,即: 当前类或父类实现的接口数量。紧接着的 n 个字节是所有的接口名称的字符串常量在常量池的索引值。
8. 字段表
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的 局部变量。字段表也分为两部分
- 第一部分是两个字节,描述字段个数
- 第二部分是每个字段的详细信息 field_info。字段表结构如下图所示:
9. 方法表
字段表结束后为方法表,方法表也是由两部分组成
- 第一部分为两个字节描述方法的个数
- 第二个部分为每个方法的详细信息。包括:方法的访问标志、方法名、方法的描述符以及方法的属性
10. 附加属性
字节码的最后一部分,存放了在文件中类或接口所定义的属性的基本信息。
参考文档:
《Java虚拟机》