Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。
Class文件本质
Class文件本质上是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
Class文件格式
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。
文件格式
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔数(魔法数字),表明当前文件是.class文件,固定0xCAFEBABE |
u2 | minor_version | 1 | 副版本号 |
u2 | major_version | 1 | 主版本号,从45开始 |
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_count | 包含的字段信息 |
u2 | methods_count | 1 | 方法数量 |
method_info | methods | methods_count | 包含的方法信息 |
u2 | attributes_count | 1 | 属性数量 |
attribute_info | attributes | attributes_count | 各种属性 |
示例
public class ClassFormat {
private int m;
public int inc() {
return m + 1;
}
}
直接查看Class文件:
我们可以使用javap -verbose ClassFormat
直接翻译Class文件,而不用人工翻译,如:
PS E:\> javap -verbose ClassFormat
警告: 二进制文件ClassFormat包含com.xiaolyuh.ClassFormat
Classfile /E:/ClassFormat.class
Last modified 2020-1-18; size 385 bytes
MD5 checksum 6fe0cc9a0e7bf5b27718cb7e42ff6bfd
Compiled from "ClassFormat.java"
public class com.xiaolyuh.ClassFormat
minor version: 0 // 次版本号
major version: 52 // 主版本号
flags: ACC_PUBLIC, ACC_SUPER // 类的访问标志
Constant pool: // 常量池
#1 = Methodref #4.#18 // java/lang/Object."":()V [从1开始]
#2 = Fieldref #3.#19 // com/xiaolyuh/ClassFormat.m:I
#3 = Class #20 // com/xiaolyuh/ClassFormat [类索引(this_class)表示引用20索引位]
#4 = Class #21 // java/lang/Object [父类索引(super_class)]
#5 = Utf8 m // 成员变量m的简单名称
#6 = Utf8 I // 表示int类型
#7 = Utf8 // 实例构造器
#8 = Utf8 ()V // 没有参数,没有返回值的方法
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/xiaolyuh/ClassFormat;
#14 = Utf8 inc // 方法inc的简单名称
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 ClassFormat.java
#18 = NameAndType #7:#8 // "":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/xiaolyuh/ClassFormat
#21 = Utf8 java/lang/Object
{
public com.xiaolyuh.ClassFormat(); // 构造函数
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1 // args_size=1的原因是this变量
0: aload_0 // 将局部变量槽 0(即this指针)的元素入栈
1: invokespecial #1 // Method java/lang/Object."":()V
4: return // 方法正常返回
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/xiaolyuh/ClassFormat;
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 Lcom/xiaolyuh/ClassFormat;
}
SourceFile: "ClassFormat.java"
更多指令请参考深入理解JVM - 虚拟机字节码指令集。
常量池
常量池中的第一个常量有特殊含义,所以常量池计数是从1开是的,其他计数是从0开始,如:接口索引集合。如上图,常量池计数是16,表示有22个常量,排除第一个,表示有21个常量,索引值范围为1~21。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
- 符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
访问标志
用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
字段访问标志:
描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。
描述符表:
java类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
long | J |
float | F |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
方法描述符:
方法 | 方法描述符 |
---|---|
void m(int i, float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
方法表集合
描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。
与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”。
属性表集合
存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。
Code属性表的结构:
- max_stack代表了操作数栈(Operand Stack)深度的最大值。
- max_locals代表了局部变量表所需的存储空间。max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
- code_length和code用来存储Java源程序编译后生成的字节码指令。
参考
《深入理解JAVA虚拟机》