Class文件结构总结
根据 Java 虚拟机规范,Class 文件通过 ClassFile
定义,有点类似 C 语言的结构体。我们之前都是使用javap
命令来对字节码文件进行反编译查看的,我们可以使用WinHex软件(Mac平台可以使用010 Editor)来以十六进制查看字节码文件。
通过分析ClassFile的内容,我们可以知道Class文件的组成如下:
下面这张图是通过 IDEA 插件 jclasslib
查看的,你可以更直观看到 Class 文件结构。
使用 jclasslib
不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。
魔数(Magic Number)
首先,我们可以看到,前4个字节(共32位)组成了魔数(其实就是表示这个文件是一个JVM可以运行的字节码文件,除了Java以外,其他某些文件中也采用了这种魔数机制来进行区分,这种方式比直接起个文件扩展名更安全)
字节码文件的魔数为:CAFEBABE
Class文件版本号
面4个字节存储的是字节码文件的版本号,注意前两个是次要版本号(现在基本都不用了,都是直接Java8、Java9这样命名了),后面两个是主要版本号,这里我们主要看主版本号,比如上面的就是34,注意这是以16进制表示的,我们把它换算为10进制后,得到的结果为:34 -> 3*16 + 4 = 52
,其中52
代表的是JDK8
编译的字节码文件(51是JDK7、50是JDK6、53是JDK9,以此类推)
注意:jdk版本号是向下兼容的
常量池
紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
-
类和接口的全限定名
-
字段的名称和描述符
-
方法的名称和描述符
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码格式的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量(第一章我们演示的很大的数字,实际上就是以字面量存储在常量池中的) |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型的字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 方法的符号引用 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
访问标志(Access Flag)
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
类访问和属性修饰符:
我们自定义了一个类,然后进行javap
反编译,得到的访问标志
当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 Java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
类的全限定名(Fully Qualified Class Name)是在编程中,一个类的完整名称,包括其所在的包(package)名称。例如,如果有一个类
Example
在com.example
包中,那么这个类的全限定名就是com.example.Example
。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
字段表集合(Fields)
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表) 的结构:
-
access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 -
name_index: 对常量池的引用,表示的字段的名称;
-
descriptor_index: 对常量池的引用,表示字段和方法的描述符;
-
attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
-
attributes[attributes_count]: 存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
方法表集合(Methods)
methods_count 表示方法的数量,而 method_info 表示方法表。
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的) 结构:
方法表的 access_flag 取值:
注意:因为volatile
修饰符和transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志
属性表集合(Attributes)
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。