深入理解Java Class文件格式
Class文件的位置和作用
JVM不会理解我们写的Java源文件, 我们必须把Java源文件编译成class文件, 才能被JVM识别, 对于JVM而言, class文件相当于一个接口, 理解了这个接口, 能帮助我们更好的理解JVM的行为;另一方面, class文件以另一种方式重新描述了我们在源文件中要表达的意思, 理解class文件如何重新描述我们编写的源文件, 对于深入理解Java语言和语法都是很有帮助的。
在整个Java技术体系结构中, class文件处于中间的位置, 对于理解整个体系有着承上启下的作用。
Class文件格式概述
class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。 我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做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_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
class文件中的魔数和版本号
魔数(magic)
在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。
版本号(minor_version,major_version)
紧接着魔数的四个字节是class文件的此版本号和主版本号。 随着Java的发展, class文件的格式也会做相应的变动。 版本号标志着class文件在什么时候, 加入或改变了哪些特性。 举例来说, 不同版本的javac编译器编译的class文件, 版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同, 一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件, 而低版本的JVM不能识别高版本的javac编译器编译的class文件。 如果使用低版本的JVM执行高版本的class文件, JVM会抛出java.lang.UnsupportedClassVersionError 。
class文件中的常量池概述
常量池是class文件中的一项非常重要的数据。 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。 字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name),字段的名称和描述符(Descriptor),方法的名称和描述符。
常量池中几乎包含类中的所有信息的描述, class文件中的很多其他部分都是对常量池中的数据项的引用,比如this_class, super_class, field_info, attribute_info
等, 另外字节码指令中也存在对常量池的引用, 这个对常量池的引用当做字节码指令的一个操作数。 此外, 常量池中各个项也会相互引用。
常量池计数器(constant_pool_count)
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。 常量池是由一组constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。常量池计数器constant_pool_count 的值 =constant_pool表中的成员数+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count时才会被认为是有效的。
常量池数据区(constant_pool[contstant_pool_count-1])
常量池,constant_pool是一种表结构,它包含 Class 文件结构及其子结构中引用的所有字符串常量、 类或接口名、字段名和其它常量。 每个数据项叫做一个XXX_info
项, 比如, 一个常量池中一个CONSTANT_Utf8
类型的项, 就是一个CONSTANT_Utf8_info
。除此之外, 每个info项中都有一个标志值(tag), 这个标志值表明了这个常量池中的info项的类型是什么, 从上面的表格中可以看出, 一个CONSTANT_Utf8_info
中的tag值为1, 而一个CONSTANT_Fieldref_info
中的tag值为9 。
常量池中数据项类型 | 类型标志 | 类型描述 |
---|---|---|
CONSTANT_Utf8 | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer | 3 | int类型字面值 |
CONSTANT_Float | 4 | float类型字面值 |
CONSTANT_Long | 5 | long类型字面值 |
CONSTANT_Double | 6 | double类型字面值 |
CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
CONSTANT_String | 8 | String类型字面值 |
CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref | 10 | 对一个类中声明的方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 对一个接口中声明的方法的符号引用 |
CONSTANT_NameAndType | 12 | 对一个字段或方法的部分符号引用 |
CONSTANT_MethodHandle | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic | 18 | 表示用invokedynamic指令所使用的引导方法(Bootstrap Method),引导方法使用动态调用名称(Dynamic Invocation Name),参数和请求返回类型以及可以选择性的附加被称为静态参数(static arguments) 的常量序列。 |
class文件中的特殊字符串
类的全限定名
在常量池中, 一个类型的名字并不是我们在源文件中看到的那样, 也不是我们在源文件中使用的包名加类名的形式。 源文件中的全限定名和class文件中的全限定名不是相同的概念。 源文件中的全新定名是包名加类名, 包名的各个部分之间,包名和类名之间, 使用点号分割。 如Object类, 在源文件中的全限定名是java.lang.Object 。 而class文件中的全限定名是将点号替换成“/” 。 例如, Object类在class文件中的全限定名是 java/lang/Object 。
字段的名称和描述符
void和基本数据类型在描述符
基本数据类型和void类型 | 类型的对应字符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
void | V |
引用类型(类和接口,枚举)
|
|
如 Object在描述符中的对应字符串是: Ljava/lang/Object;
ArrayList在描述符中的对应字符串是: Ljava/lang/ArrayList;
自定义类型com.example.Person在描述符中的对应字符串是: Lcom/example/Person。
数组类型
|
|
如 int[]
类型的对应字符串是: [I
;
int
[][]类型的对应字符串是: [[I
;
Object[]
类型的对应字符串是: [Ljava/lang/Object
;
Object[][][]
类型的对应字符串是: [[[Ljava/lang/Object
。
方法的名称和描述符
|
|
特殊方法的方法名,如类的构造方法的方法名使用字符串 <init>
表示, 而静态初始化方法的方法名使用字符串 <clinit>
表示。 除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同。
class文件中的访问标志信息
访问标志(access_flags)紧接着常量池后,占有两个字节,总共16位
当JVM在编译某个类或者接口的源代码时,JVM会解析出这个类或者接口的访问标志信息,然后,将这些标志设置到访问标志(access_flags)这16个位上。
a. 我们知道,每个定义的类或者接口都会生成class文件(这里也包括内部类,在某个类中定义的静态内部类也会单独生成一个class文件)。 对于定义的类,JVM在将其编译成class文件时,会将class文件的访问标志的第11位设置为1 。第11位叫做ACC_SUPER标志位;对于定义的接口,JVM在将其编译成class文件时,会将class文件的访问标志的第8位 设置为 1 ,第8位叫做ACC_INTERFACE标志位;
b. class文件表示的类或者接口的访问权限有public类型的和包package类型的。如果类或者接口被声明为public类型的,那么,JVM将其编译成class文件时,会将class文件的访问标志的第16位设置为1 。第16位叫做ACC_PUBLIC标志符;
c. 类是否为抽象类型的,即我们定义的类有没有被abstract关键字修饰,即我们定义的类是否为抽象类。
如果我们形如:
|
|
定义某个类时,JVM将它编译成class文件的时候,会将class文件的访问标志的第7位设置为1 。第7位叫做ACC_ABSTRACT标志位。 另外值得注意的是,对于定义的接口,JVM在编译接口的时候也会对class文件的访问标志上的ACC_ABSTRACT标志位设置为 1;
d. 该类是否被声明了final类型,即表示该类不能被继承。
此时JVM会在编译class文件的过程中,会将class文件的访问标志的第12位设置为 1 。第12位叫做ACC_FINAL标志位;
e. 如果我们这个class文件不是JVM通过Java源代码文件编译而成的,而是用户自己通过class文件的组织规则生成的,那么,一般会对class文件的访问标志第4位设置为 1 。通过JVM编译源代码产生的class文件此标志位为 0,第4位叫做ACC_SYNTHETIC标志位;
f. 枚举类,对于定义的枚举类如:public enum EnumTest{….},JVM也会对此枚举类编译成class文件,这时,对于这样的class文件,JVM会对访问标志第2位设置为 1 ,以表示它是枚举类。第2位叫做ACC_ENUM标志位;
g. 注解类,对于定义的注解类如:public @interface{…..},JVM会对此注解类编译成class文件,对于这样的class文件,JVM会将访问标志第3位设置为1,以表示这是个注解类,第3位叫做ACC_ANNOTATION标志位。
class文件中的this_class
一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过 类索引 这个数据项来确定。
class文件中的super_class
Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。如果没有显式的继承一个,也就是说如果当前类是直接继承Object的, 那么super_class值为0 。 如果一个索引值为0, 那么就说明这个索引不引用任何常量池中的数据项, 因为常量池中的数据项是从1开始的。 也就是说, 如果一个类的class文件中的super_class为0 , 那么就代表该类直接继承Object类。
class文件中的interfaces_count和interfaces
紧接着super_class的是interfaces_count, 表示当前类所实现的接口的数量或者当前接口所继承的超接口的数量。 注意, 只有当前类直接实现的接口才会被统计, 如果当前类继承了另一个类, 而另一个类又实现了一个接口, 那么这个接口不会统计在当前类的interfaces_count中。 在interfaces_count后面是interfaces, 他可以看做是一个数组, 其中的每个数组项是一个索引, 指向常量池中的一个CONSTANT_Class_info, 这个CONSTANT_Class_info又会引用常量池中的一个CONSTANT_Utf8_info , 这个CONSTANT_Utf8_info 中存放着有当前类型直接实现或继承的接口的全限定名。 当前类型实现或继承了几个接口, 在interfaces数组中就会有几个数项与之相对应。
class文件中的fields_count和fields
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
Java中的一个Field字段应该包含信息如下:
FIeld_info的组成元素:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表集合
其中access_flags占两个字节, 描述的是字段的访问标志信息。
class文件中的methods_count和methods
方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:
注意:
methods_count描述的是当前的类中定义的方法的个数,这里包括静态方法, 但不包括从父类继承的方法。 如果当前class文件是由一个接口生成的, 那么这里的methods_count描述的是接口中定义的抽象方法的数量, 我们知道, 接口中定义的方法默认都是公有的。此外需要说明的是, 编译器可能会在编译时向class文件增加额外的方法, 也就是说, class文件中的方法的数量可能多于源文件中由用户定义的方法。 举例来说: 如果当前类没有定义构造方法, 那么编译器会增加一个无参数的构造函数
位于methods_count下面的数据叫做methods , 可以把它看做一个数组, 数组中的每一项是一个method_info 。这个数组中一共有methods_count个method_info , 每个method_info 都是对一个方法的描述。
上图中的method_info结构体的定义,该结构体的定义跟描述field字段 的field_info结构体的结构几乎完全一致方法表的结构体由:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。
访问标志(access_flags):
method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息。
名称索引(name_index):
紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。
描述索引(descriptor_index):
描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为:(参数数据类型描述列表)返回值数据类型。所谓的方法描述符,实质上就是指用一个什么样的字符串来描述一个方法
属性表(attribute_info)集合:
这个属性表集合非常重要,方法的实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现。这些信息包括:这个方法的代码实现,即方法的可执行的机器指令;
这个方法声明的要抛出的异常信息;这个方法是否被@deprecated注解表示;这个方法是否是编译器自动生成的。
属性表之Code类型
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。 Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、 字段、 方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
|
|
机器指令(code):JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分。
异常处理跳转信息:如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转。一个异常处理器(exception_info)的意思是: 如果偏移量从start_pc到end_pc之间的字节码出现了catch_type描述的类型的异常, 那么就跳转到偏移量为handler_pc的字节码处去执行。如果catch_type为0, 就代表不引用任何常量池项(常量池中的项是从1开始计的), 那么这个exception_info用于实现finally子句。
exception_info中的各个字段:
start_pc是从字节码(Code属性中的code部分)起始处到当前异常处理器起始处的偏移量。
end_pc是从字节码起始处到当前异常处理器末尾的偏移量。
handler_pc是指当前异常处理器用来处理异常(即catch块)的第一条指令相对于字节码开始处的偏移量。
catch_type是一个常量池索引, 指向常量池中的一个CONSTANT_Class_info数据项, 该数据项描述了catch块中的异常的类型信息。这个类型必须是java.lang.Throwable的或其子类。
Java源码行号和机器指令的对应关系—LineNumberTable属性表:编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
局部变量表描述信息—-LocalVariableTable属性表:局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下:
Exceptions类型
Exceptions类型的属性表(attribute_info)结构体由一下元素组成:属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示”Exceptions”字符串的常量池项;属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,表示这个异常的完全限定名称;
如果某个方法定义中,没有声明抛出异常,那么,表示该方法的方法表(method_info)结构体中的属性表集合中不会有Exceptions类型的属性表;换句话说,如果方法声明了要抛出的异常,方法表(method_info)结构体中的属性表集合中必然会有Exceptions类型的属性表,并且该属性表中的异常数量不小于1。
我们假设异常数量中的值为 N,那么后面的异常名称索引的数量就为N,它们总共占有的字节数为N*2,而异常数量占有2个字节,那么将有下面的这个关系式:
属性长度(attribute_length)中的值= 2 + 2*异常数量(number_of_exceptions)中的值
Exceptions类型的属性表(attribute_info)的长度=2 + 4 + 属性长度(attribute_length)中的值
Synthetic类型
Synthetic属性可以出现在filed_info中, method_info中和顶层的ClassFile中, 分别表示这个字段, 方法或类不是有用户代码生成的(即不存在与源文件中), 而是由编译器自动添加的。 例如, 编译器会为内部类增加一个字段, 该字段是对外部类对象的引用; 如果一个不定义构造方法, 那么编译器会自动添加一个无参数的构造方法
Deprecated类型
Deprecated属性可以存在于filed_info中, method_info中和顶层的ClassFile中, 分别表示这个字段, 方法或类已经过时。 这个属性用来支持源文件中的@deprecated注解。 也就是说, 如果在源文件中为一个字段, 方法或类标注了@deprecated注解, 那么编译器就会在class文件中为这个字段, 方法或类生成一个Deprecated属性 。attribute_length永远为0 , 因为这个属性只是一个标志信息, 用来表示字段, 方法, 类已经过时, 而不具有任何实质性的属性信息。