概述
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有任何的分隔符。当遇到需要8位字节以上空间的数据项时,则会按照高位在前(大端序)的方式分割成若干个8位字节进行存储。
按照虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的结构来表示数据,这种结构中只有两种数据类型:无符号数与表。
无符号数
无符号数属于基本的数据类型,u1、u2、u4、u8分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表
表由多个无符号数或其他表作为数据项构成的复合数据类型,习惯性地以“_info“结尾。表是用于描述有层次关系的复合结构数据,整个Class文件就是一张表。
整体结构
整个class文件可以看成一张表,表的第一层级如下表:

Class文件具体结构分析
这一节会详细介绍类文件中包含的各种表。主要介绍常量池、字段表、方法表。
魔数与文件版本
前1-4字节被称为魔数,魔数值来唯一确定文件类型,Class文件魔数是:0xCAFEBABE。
minor_version和major_version:5-6个字节代表次版本号,7-8个字节代表主版本号。
常量池(constant_pool)
可以理解为Class文件的字符资源库,它存储着Class文件中其他表需要用的常量(字面量和符号引用)。
constant_pool_count:常量池的数目。容量计数从1开始,也就是说常量池第一个位置是空的,如果constant_pool_count值为1,则表示常量数为0。这样做的目的是,当其他地方引用常量池时,如果其索引值为0,则表示它不引用任何一个常量池项目。这样的设计可以做到这种语义表达。
constant_pool:常量池,Class类文件中出现的第一个表类型数据,存储字面量(Literal)与符号引用(Symbolic Reference)。
- 字面量(Literal):字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
- 符号引用(Symbolic Reference):包括以下字符常量,类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Field Descriptor)、方法的名称和描述符(Method Descriptors)
常量池中的每一种常量都是一张表,每张表具体结构不同,具体的细节这里不再阐述,这里仅举一个例子说明,具体的细节可以直接查阅官方文档 Java Virtual Machine Specification 4.4 节的内容。
考虑以下反编译出的字节码:

注意到#3位置的常量是类常量,结构为
1 | struct CONSTANT_Class_info { |
而#17自然代表的就是CONSTANT_Utf8_info
1 | struct CONSTANT_Utf8_info { |
类的访问标志(access_flags)
常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。
类索引、父类索引、接口索引集合
这个数据项主要用于确定这个类的继承关系。
其中类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口。
字段表(field_info)
字段表用于描述接口或者类中声明的变量。结构如下:
1 | struct field_info { |
- access_flags:标记字段的访问权限和属性,包括public、static、volatile等。
- name_index:下标值指向常量池,代表了字段的非限定名的字符字面量。(例如
private int number;对应的值就是字符串“number”) - descriptor_index:下标值指向常量池,代表了字段的描述符的字符字面量。(例如
private Test test;对应的值就是字符串“Lcom/xybean/test/Test;”) - attributes_count:当前字段额外属性的数量
- attributes[attributes_count]:额外属性。例如对于
private static int number = 1;会保存意向名称为ConstantValue的属性,其值指向123。
关于字段的描述符,具体的字符表如下:

举例来说,String 的描述符为Ljava/lang/String ,int[] 的描述符为 [I ,二维数组 int[][] 的描述符为[[I。
方法表(method_info)
方法表用于描述class文件中定义的方法,其结构如下:
1 | struct method_info { |
可以发现其结构与字段表是一致的,区别在于在方法中不能用volatile和transient关键字修饰,所以这两个标志不能用在方法表中。同时因为方法中添加了字段不能使用的访问标志,比如方法可以使用synchronized、native、strictfp、abstract关键字修饰,所以在方法表中就增加了相应的访问标志。
方法描述符的格式形如 “(参数)返回值” ,例如方法 String getString(int[] a, long b) 的方法描述符为([IJ)Ljava/lang/String 。至于方法名,显而易见就是 getString 。
在上面的描述中都没有提到方法中的可执行代码,实际上这些代码会被转换成指令字符,存储在方法表中的Code属性中,这个在后面会具体介绍。
值得一提的是, 在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名(注:Java 代码的方法特征签名只包括方法名称、参数顺序及参数类型,而字节码的特征签名还包括方法返回值以及受查异常表),特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此 Java 语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在 Class 文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个 Class 文件中的。
属性表(attribute_info)
在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
常用的属性如下表:

具体的细节这里不再赘述,可以直接参考官方文档 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7
字节码指令简介
Java虚拟机的指令由一个字节长度的、 代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。
如果不考虑异常处理的话,JVM的解释器可以用下面的伪代码当做最基本的执行模型来理解:
1 | do { |
字节码与数据类型
在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。其中i代表int,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。 也有一些指令的助记符中没有明确地指明操作类型的字母,如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。 还有另外一些指令,如无条件跳转指令goto则是与数据类型无关的。
字节码分类
加载存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
运算指令:运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
类型转换指令:类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。
对象创建与访问指令:虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。 对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
操作数栈管理指令:用于控制操作数栈的出入。
控制转移指令:控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
方法调用和返回指令:执行方法调用与方法返回。
同步指令:Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。 虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。 当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持
字节码指令速查
上面两节对字节码指令做了概述,由于指令太多,并未介绍细节,需要具体了解细节的,可以直接查表。
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.1.9
从方法表到字节码指令
上面提到过方法表中的Code属性存储着方法中代码对应的字节码指令序列,这一节就举例介绍下字节码是如何执行的。
Java文件的源代码如下:
1 | // Test.java |
编译后用javap分析得到Code属性表如下:
1 | 0: iconst_2 // 将int类型数据 2 压入到操作数栈中 |
这里对new一个对象的系列指令做一个细节的介绍:
第一步new操作,会创建指定类型的对象实例、对其进行默认初始化,并且将指向该实例的一个引用压入操作数栈顶,此时栈情况由顶向下为(Test引用);
第二步dup,复制栈顶引用,此时栈情况为(Test引用 <- Test引用);
第三步iload_3,去取出b的值置于栈顶,此时栈情况为(b <- Test引用 <- Test引用);
第四步invokespecial #4,以栈顶两个数为参数,调用初始化方法,此时栈情况为(Test引用);
第五步areturn,将栈顶的引用返回,此时栈情况为();
更多的细节可以参考:关于JVM字节码中dup指令的问题?
异常处理
异常处理与普通的方法执行相关却略有不同,因此这里单独介绍一下异常处理在字节码执行层面的原理。
惯例先看一段Java代码
1 | // Test.java |
下面是javap的分析结果,重点关注Exception table

异常表中的值表示的意思是在执行第2-4条指令的时候,如果抛出了Exception,就跳转执行第7条指令,可以发现第7条指令开始就是catch块中的代码,而指令10则是catch块外的代码。
指的注意的是这里athrow指令的作用,其中异常初期器指的就是异常表:

参考资料
Java Virtual Machine Specification > Chapter 4. The class File Format