目录
- 深入剖析Class文件结构
- 一、初探class文件
- 二、class文件结构剖析
- (一) 魔数
- (二) 版本号
- (三) 常量池
- 1. CONSTANT_Integer_info和CONSTANT_Float_info
- 2.CONSTANT_Long_info与CONSTANT_Double_info
- 3. CONSTANT_Utf8_info
- 4. CONSTANT_String_info
- 5. CONSTANT_Class_info
- 6. CONSTANT_NameAndType_info
- 7. CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
- 8.CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info
- (四) Access flags
- (五) this_class、super_name、interfaces
- (六) 字段表
- (七) 方发表
- (八) 属性表
深入剖析Class文件结构
一、初探class文件
class文件使用javac
编译.java
文件后得到,可以直接采用十六进制工具查看这个文件。
二、class文件结构剖析
- Java虚拟机规定使用
u1
、u2
、u4
三种数据结构来表示1、2、4
字节无符号整数,相同类型的若干条数据集合用表(table)
的形式来存储。表是一个变长的结构,由代表长度的表头n和紧随的n个数据项组成。class文件采用类似C语言的结构体来存储数据。
- class文件由十个部分组成:
- 魔数 Magic Number
- 版本号 Minor&Major Version
- 常量池 Constant Pool
- 类访问标记 Access Flag
- 类索引 This Class
- 超类索引 Super Class
- 接口表索引 Interface
- 字段表 Field
- 方发表 Method
- 属性表 Attribute
(一) 魔数
我们经常通过文件后缀来识别文件类型。比如看到一个.jpg
的文件,就知道是图片。但是使用后缀识别很不靠谱,后缀可以被任意修改,那如何根据文件内容本身来识别文件的类型呢?使用魔数(Magic Number)实现。
文件格式的制定者可以自由的选择魔数值,只要魔数值还没有被广泛才用过且不会引起混淆即可。使用十六进制工具打开class文件,首先看到的就是魔数0xCAFEBABE
(咖啡宝贝)。该魔数是JVM识别.class
文件的标志,虚拟机在加载类文件之前会先检查这四个字节,如果不是0xCAFEBBE
,就会抛出java.lang.ClassFormateError
异常。
(二) 版本号
在魔数之后的四个字节分别表示副版本号(Minor Version)和主版本号(Major Version)。
Java8的主版本号是52(0x34)
,如果类文件的版本号高于JVM自身的版本号,加载类就会被直接抛出java.lang.UnsupportedClassVersionError
异常。
每次Java发布大版本,主版本会加1.
(三) 常量池
紧随版本号之后的是常量池数据区域。对于JVM字节码来讲,如果操作是很常用的数字,比如0,这些操作数是内嵌到字节码中的。如果是字符串常量和较大的整数等,class文件则会把这些操作数存储在常量池(Constant Pool)中,当使用这些操作数时,会根据常量池的索引位置来查找。
常量池结构如下:
常量池分为两部分:
- 常量池大小(cp_info_count) 2字节无符号整数
常量池是class文件中第一个出现的变长结构。假设常量池大小为n,常量池真正的有效索引是1 ~ n-1
。0属于保留索引,可供特殊情况使用。 - 常量池项
cp_info
集合:最多包含n-1
个元素。为什么是最多呢?long和double类型的常量会占用两个索引位置,如果常量池包含了这两种类型的元素,实际的常量池项的元素个数比n-1
小。
常量池构成图
常量池中每个常量项的结构:
每个cp_info
的第一个字节表示常量项的类型(tag),接下来的几个字节表示常量项的具体内容。
Java虚拟机目前一共定义了14中常量项tag类型,这些常量名都已CONSTANT
开头,以info
结尾。
如果想查看类文件的常量池,可以使用javap
命令加上-v
选项。
1. CONSTANT_Integer_info和CONSTANT_Float_info
CONSTANT_Integer_info和CONSTANT_Float_info这两种结构分别用来表四int和float类型的常量,两者的结构很类似,都用4个字节来表示具体的数值常量,它们的结构定义如下
Java语言规范还定义了boolean
、byte
、short
和char
类型的变量,在常量池中都会被当作int
来处理。例如:
2.CONSTANT_Long_info与CONSTANT_Double_info
这两种结构分别用来表示long
和double
类型的变量,二者都用8个字节表示具体的常量数值,它们的结构如下面的代码所示。
前面提到过,CONSTANT_Long_info和CONSTANT_Double_info占用两个常量池位置,可以看到常量池大小为22,常量a占用了 #3和 #4两个位置,下一个常量项Fieldref从索引值5开始
3. CONSTANT_Utf8_info
CONSTANT_Utf8_info存储了字符串的内容,结构如下
它由三部分构成:第一个字节是tag
,值固定为1;tag
之后的两个字节length
并不是表示字符串有多少个字符,而是表示第三部分byte
数组的长度;第三部分是采用MUTF-8
编码的长度为length
的字节数组。
加入存储字符串"hello",则存储结构为
4. CONSTANT_String_info
CONSTANT_String_info用来表示java.lang.String
类型的常量对象。它与CONSTANT_Utf8_info
的区别是CONSTANT_Utf8_info
存储了字符串真正的内容,而CONSTANT_String_info
并不包含字符串的内容,仅仅包含一个指向常量池中CONSTANT_Utf8_info
常量类型的索引。
CONSTANT_String_info
由两部分构成,第一个字节是tag,值为8,tag后面的两个字节是一个名为string_index
的索引值,指向常量池中的CONSTANT_Utf8_info
,这个CONSTANT_Utf8_info
存储的才是真正的字符串常量内容。
5. CONSTANT_Class_info
该结构用来表示类或接口,它的结构与CONSTANT_String_info
非常相似.
它由两部分组成,第一个字节是tag
,值固定为7 , tag后面的两个字节name_index
是一个常量池索引,指向CONSTANT_Utf8_info
常量,这个字符串存储的是类或接口的全限定名。例如:
6. CONSTANT_NameAndType_info
CONSTANT_NameAndType_info结构用来表示字段或者方法,结构为;
CONSTANT_NameAndType_info结构由三部分组成,第一部分tag
值固定为12,后面的两个部分name_index
和descriptor_index
都指向常量池中的CONSTANT_Utf8_info
的索引,name_index
表示字段或方法的名字,descriptor_index
是字段或方法的描述符,用来表示一个字段或方法的类型。例如方法:
对应的CONSTANT_NameAndType_info结构为
7. CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
这三种结构比较类似
下面以CONSTANT_Methodref_info
为例来进行讲解,它用来描述一个方法。它由三部分组成:第一部分是tag
值,固定为10
;第二部分是class_index
,是一个指向CONSTANT_Class_info
的常量池索引值,表示方法所在的类信息;第三部分是name_and_type_index
,是一个指向CONSTANT_NameAndType_info
的常量池索引值,表示方法的方法名、参数和返回值类型。例如以下代码:
testMethod
对应的Methodref
的class_index
为2,指向类名为“HelloWorldMain”
的类,name_and_type_index
为20,指向常量池中下标为20的NameAndType
索引项,对应的方法名为“testMethod”
,方法类型为“(ILjava/lang/String;)V”
。
8.CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info
从JDK1.7开始,为了更好地支持动态语言调用,新增了3种常量池类型(CONSTANT_MethodType_info
、CONSTANT_MethodHandle_info
和CONSTANT_InvokeDynamic_info
)。以CONSTANT_InvokeDynamic_info
为例,CONSTANT_InvokeDynamic_info
的主要作用是为invokedynamic指令提供启动引导方法,它的结构如下所示。
第一部分为tag
,值固定为18;第二部分为bootstrap_method_attr_index
,是指向引导方法表bootstrap_methods[]
数组的索引。第三部分为name_and_type_index
,是指向索引类常量池里的CONSTANT_NameAndType_info
的索引,表示方法描述符。
(四) Access flags
紧随常量池之后的区域是访问标记(Access flags
),用来标识一个类为final
、abstract
等,由两个字节表示,总共有16个标记位可供使用,目前只使用了其中的8个。
完整的访问标记符含义为:
这些访问标记并不是可以随意组合的,比如ACC_PUBLIC
、ACC_PRIVATE
、ACC_PROTECTED
不能同时设置,ACC_FINAL
和ACC_ABSTRACT
也不能同时设置,否则会违背语义。
(五) this_class、super_name、interfaces
这三部分用来确定类的继承关系,this_class
表示类的索引,super_name
表示直接父类的索引,interfaces
表示类或者接口的直接父接口。
this_class
是一个指向常量池的索引,表示类或者接口的名字,用两字节表示。
(六) 字段表
紧随接口索引表之后的是字段表(fields),其中定义的字段会被存储到这个集合中,包括静态和非静态的字段,它的结构可以用下面的伪代码表示。
字段表也是一个变长的结构,fiedls_count表示field的数量,接下来的fields表示字段集合,共有fields_count个,每一个字段用field_info结构表示,稍后会进行介绍。
1.字段field_info结构
每个字段field_info的格式如下
字段结构分为4个部分:第一部分access_flags
表示字段的访问标记,用来标识是public
、private
还是protected
,是否是static
,是否是final
等;第二部分name_index
用来表示字段名,指向常量池的字符串常量;第三部分descriptor_index
是字段描述符的索引,指向常量池的字符串常量;最后的attributes_count
、attribute_info
表示属性的个数和属性集合。
2. 字段访问标记
与类一样,字段也拥有访问标记,但字段的访问标记更丰富,共有9种。
如果在类中定义了字段public static final int DEFAULT_SIZE = 128
,编译后DEFAULT_SIZE
字段在类文件中存储的访问标记值为0 x0019
,则它的访问标记为ACC_PUBLIC |ACC_STATIC | ACC_FINAL
,表示它是一个public static final
类型的变量。
同之前介绍的类访问标记一样,字段访问标记并不是可以随意组合的,比如ACC_FINAL
和ACC_VOLATILE
也不能同时设置,否则会违背语义。
3. 字段描述符
字段描述符field descriptor用来表示某个field的类型,在JVM中定义一个int类型的字段时,类文件中存储的类型并不是字符串int,而是更精简的字母I。
根据类型不同,字段描述符分为三大类:
- 原始类型,
byte、int、char、float
等这些简单类型使用一个字符来表示,比如J
对应long
类型,B
对应Byte
类型。 - 引用类型使用
L;
的方式来表示,为了防止多个连续的引用类型描述符出现混淆,引用类型描述符最后都加了一个;
作为结束,比如字符串类型String
的描述符为Ljava/lang/String;
。 - JVM使用一个前置的
[
表示数组类型,如intp[]
类型的描述符为[I
,字符数组String[]
的描述符为[Ljava/lang/String;
。而多维数组描述符只是多加了几个[
而已,比如Object[][]
的描述符为[[Ljava/lang/Object;
。
完整的字段类型描述符隐射表
4. 字段属性
与字段相关的属性包括ConstantValue
、Synthetic
、Signature
、Deprecated
、Runtime-Visible Annotations
和RuntimeInvisibleAnnotations
这6个,比较常见的是ConstantValue
属性,用来表示一个常量字段的值。
(七) 方发表
方发表的作用和字段表非常相似,类中定义的方法被存储在这里,方发也是一个变长结构,表结构
其中methods_count
表示方法的数量,接下来的methods
表示方法的集合,共有methods_count
个,每一个方法用method_info
结构表示。
1. 方法method_info结构
方法method_info
结构分为四部分:第一部分access_flags
表示方法的访问标记,用来标记是public
、private
还是protected
,是否是static
,是否是final
等;接下来的name_index
、descriptor_index
分别表示方法名和方法描述符的索引值,指向常量池的字符串常量;attributes_count
和attribute_info
表示方法相关属性的个数和属性集合,包含了很多有用的信息,比如方法内部的字节码就存放在Code
属性中。
2. 方法访问标记
方法的访问标记比类和字段的访问标记更丰富,一共12种。
例子:
生成的类文件中,foo
方法的访问标记等于0x002a
(ACC_PRIVATE |ACC_STATIC|ACC_SYNCHRONIZED
),表示这是一个private static synchronized
的方法。
3. 方法名与描述符
紧随方法访问标记的是方法名索引name_index
,指向常量池中CONSTANT_Utf8_info
类型的字符串常量,比如有这样一个方法定义private void foo()
,编译器会生成一个类型为CONSTANT_Utf8_info
的字符串常量项,里面存储了foo
,方法名索引name_index
指向了这个常量项。
方法描述符索引descriptor_index也是指向常量池中类型为CONSTANT_Utf8_info的字符串常量项。方法描述符用来表示一个方法所需的参数和返回值,格式如下:
(参数1类型 参数2类型 参数3类型 …) 返回值类型
比如,方法Object foo(int i, double d, Thread t)
的描述符为(IDLjava/lang/Thread;) Ljava/lang/Object;
,其中I
表示第一个参数i
的参数类型int
,D
表示第二个参数d
的类型double
,Ljava/lang/Thread;
表示第三个参数t的类型Thread
,Ljava/lang/Object;
表示返回值类型Object
。
4. 方法属性表
方法属性表是method_info
结构的最后一部分。前面介绍了方法的访问标记和方法签名,还有一些重要的信息没有出现,如方法声明抛出的异常,方法的字节码,方法是否被标记为deprecated等,属性表就是用来存储这些信息的。与方法相关的属性有很多,其中比较重要的是Code和Exceptions属性,其中Code属性存放方法体的字节码指令,Exceptions属性用于存储方法声明抛出的异常。
(八) 属性表
在方法表之后的结构是class文件的最后一部分——属性表。属性出现的地方比较广泛,不只出现在字段和方法中,在顶层的class文件中也会出现。相比于常量池只有14种固定的类型,属性表的类型更加灵活,不同的虚拟机实现厂商可以自定义属性,属性表的结构如下所示。
与其他结构类似,属性表使用两个字节表示属性的个数attributes_count
,接下来是若干个属性项的集合,可以看作是一个数组,数组的每一项都是一个属性项attribute_info
,数组的大小为attributes_count
。每个属性项的atttribute_info
的结构如下
attribute_name_index是指向常量池的索引,根据这个索引可以得到
attribute
的名字,接下来的两部分表示info
数组的长度和具体byte
数组的内容。
虚拟机预定义了20多种属性,下面介绍字段表相关的ConstantValue
属性和方法表相关的Code
属性。
1. ConstantValue属性
ConstantValue
属性出现在字段field_info
中,用来表示静态变量的初始值,它的结构如下所示。
其中attribute_name_index
是指向常量池中值为ConstantValue
的字符串常量项,attribute_length
值固定为2,因为接下来的具体内容只会有两个字节大小。constantvalue_index
指向常量池中具体的常量值索引,根据变量的类型不同,constantvalue_index
指向不同的常量项。如果变量为long
类型,则constantvalue_index
指向CONSTANT_Long_info
类型的常量项。
2. Code属性
Code
属性是类文件中最重要的组成部分,它包含方法的字节码,除native
和abstract
方法以外,每个method
都有且仅有一个Code
属性,它的结构如下
1) 属性名索引(attribute_name_index),指向常量池中CONSTANT_Utf8_info
常量,表示属性的名字,比如这里对应的常量池的字符串常量"Code"。
2)属性长度(attribute_length)
3)max_stack
表示操作数栈的最大深度,方法执行的任意期间操作数栈的深度都不会超过这个值。它的计算规则是:有入栈的指令stack增加,有出栈的指令stack减少,在整个过程中stack的最大值就是max_stack
的值,增加和减少的值一般都是1,但也有例外:Long
和DOUBLE
相关的指令入栈stack会增加2,VOID
相关的指令则为0.
4)max_locals表示局部变量表的大小,它的值并不等于方法中所有局部变量的数量之和。当一个局部作用域结束,它内部的局部变量占用的位置就可以被接下来的局部变量复用了。
5)code_length
和code
用来表示字节码相关的信息。其中,code_length
表示字节码指令长度;code
是一个长度为code_length
字节的数组,存储真正的字节码指令。
6)exception_table_length
和exception_table
用来表示代码内部的异常表信息,如我们熟知的try-catch
语法就会生成对应的异常表。exception_table_length
表示接下来exception_table
数组的长度,每个异常项包含四个部分,可以用下面的结构表示。
其中start_pc
、end_pc
、handler_pc
都是指向code
字节数组的索引值,start_pc
和end_pc
表示异常处理器覆盖的字节码开始和结束的位置,是左闭右开区间[start_pc, end_pc)
,包含start_pc
,不包含end_pc
。handler_pc
表示异常处理handler
在code
字节数组的起始位置,异常被捕获以后该跳转到何处继续执行。catch_type
表示需要处理的catch
的异常类型是什么,它用两个字节表示,指向常量池中类型为CONSTANT_Class_info
的常量项。如果catch_type
等于0,则表示可处理任意异常,可用来实现finally
语义。
当JVM执行到这个方法 [start_pc, end_pc)
范围内的字节码发生异常时,如果发生的异常是这个catch_type
对应的异常类或者它的子类,则跳转到code
字节数组handler_pc
处继续处理。
7)attributes_count
和attributes[]
用来表示Code
属性相关的附属属性,Java虚拟机规定Code属性只能包含这四种可选属性:LineNumberTable
、LocalVariableTable
、LocalVariableTypeTable
、StackMapTable
。