Java Class文件结构:概述
想静下心来读点书,从阅读《JVM specification》[1]开始吧。一直希望一探class文件的内部结构,却无端生出了各种借口成蹉跎岁月。18年岁末是个好日子,Let’s go!
Java的class文件描述了类、接口和模块。众所周知,class文件是一个字节码文件,即按照字节(8 bits)来组织和解读内容。为了表述方便,先定义以下的术语:
u1
1个字节
u2
2个字节
u4
4个字节
表项
通过表格方式描述的项目称作“表项”。比如常量池中有14种常量,每种常量的描述方式(属性)不同,因此每种常量使用不同格式的表项来描述。以_info结尾的词即为一个表项,比如CONSTANT_Utf8_info是Utf8编码的字符串的表项,描述了一个Utf8编码的字符串在常量池中的存储格式。
全限定名
类的全限定名是指包括包名的类名,比如sdut.edu.cn.java.Test即为全限定名。
1 class文件的总体结构
class文件的结构如1[2, p165]所示,包含了10个部分。
类型
名称
数量
备注
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
interface_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
附加属性表表项
表 1: class文件结构
初看起来,class文件的内容多的吓人,但是耐心的想一下,就会释然并觉得是很自然的事情了。下面按照class文件的顺序解读其中的每一个细节(字节),并试图说明class文件为什么要这样设计:让我们一起揣测JVM的设计者当初的“小心思”,也是一件非常有意思的事情。
为了更直观的理解class的文件结构,后面的解读以1为例。
public class Person {
private int age;
public boolean isAdult() {
return age > 18;
}
}
解读class文件结构的主要工具是16进制文件编辑器,在Windows下可以使用ultroedit,editplus等工具,在Linux下可以使用xxd,ghex等。另外,JDK也提供了观察class内部结构的工具:javap,只要执行javap -v Person.class即可:
Classfile /home/subaochen/git/blog/src/java/Person.class
Last modified 2018年12月2日; size 310 bytes
MD5 checksum 3f24365f1557571a81fb3369533d20ee
Compiled from "Person.java"
public class Person
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // Person
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."":()V
#2 = Fieldref #3.#17 // Person.age:I
#3 = Class #18 // Person
#4 = Class #19 // java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 isAdult
#12 = Utf8 ()Z
#13 = Utf8 StackMapTable
#14 = Utf8 SourceFile
#15 = Utf8 Person.java
#16 = NameAndType #7:#8 // "":()V
#17 = NameAndType #5:#6 // age:I
#18 = Utf8 Person
#19 = Utf8 java/lang/Object
{
public Person();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public boolean isAdult();
descriptor: ()Z
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: bipush 18
6: if_icmple 13
9: iconst_1
10: goto 14
13: iconst_0
14: ireturn
LineNumberTable:
line 5: 0
StackMapTable: number_of_entries = 2
frame_type = 13 /* same */
frame_type = 64 /* same_locals_1_stack_item */
stack = [ int ]
}
SourceFile: "Person.java"
目前你可能对Javap的输出不甚了了,在后面的讲解中我们会逐步弄清楚其中的每一个细节。
2 魔数
class文件的前4个字符是固定内容的“魔数”,通过ghex观察可见如1所示,class文件的魔数是0xCAFEBABE
1
中文翻译过来是“咖啡宝贝”,这其中的故事可以参考Java之父James Gosling的解释:https://en.wikipedia.org/wiki/Java_class_file#Magic_Number
。
魔数的作用是表征文件是一个合法的Java类文件,任何不以魔数开头的字节码文件都是非法的class文件,虚拟机将拒绝执行。
图 1: class的魔数
3 版本号
魔数之后的4个字节表示“版本号”,其中前两个字节是次版本号(minor version),后两个字节是主版本号(major version)。如2所示,Person.class文件的版本号是0x00000037,即主版本号是0x0037(对应的十进制数是55),次版本号是0x0000(对应的十进制数是0),即Person.class的版本号翻译成十进制为55.0。2列出了JDK定义的主版本号,对照可以看出,Person.class是使用Java SE 11编译而成的。
JDK编译器版本
十六进制主版本号
十进制主版本号
JDK 1.1
0x2D
45
JDK 1.2
0x2E
46
JDK 1.3
0x2F
47
JDK 1.4
0x30
48
Java SE 5
0x301
49
Java SE 6
0x32
50
Java SE 7
0x33
51
Java SE 8
0x34
52
Java SE 9
0x35
53
Java SE 10
0x36
54
Java SE 11
0x37
55
Java SE 12
0x38
56
表 2: JDK的版本号定义
图 2: class的版本号
class文件的版本号使用了4个字节来表示,可以表达的最大版本号是65535.65535,可见Java的雄心壮志:当前Java11的版本号是55.0,按照目前的开发速度,Java的版本号可以用到数万年之后。
版本号的作用是表明该class文件是由哪个版本的编译器生成的,因此需要相应版本的java虚拟机来解释执行。显然,高版本的Java虚拟机可以解释执行低版本的class文件,反之则不然。
4 小结
简单的开个头。解读class文件的结构需要一点点耐心,需要一点点的技巧。魔数和版本号是固定长度的,都很容易理解,接下来解读class文件中可能是内容最多但不是最复杂的部分:常量池,TBD。
引用
1Oracle, “JVM specification“.
2周志明, 深入理解Java虚拟机 2 (机械工业出版社, 2018).
0