Java类文件结构——以HelloWorld为例逐字节分析
本文旨在通过对HelloWorld
代码编译后的类文件进行逐字节分析,讲解Class类文件的结构。
准备工作
先准备一段 Java 代码
1 | package com.raining; |
然后,用 javac
编译,得到class
文件
class
文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中。注意是大端机(高位在前)的存储方式。
准备工具WinHex,WinHex可以轻松的以十六进制的格式打开文本文件,方便我们查看class
文件中的二进制码。使用WinHex打开class文件,界面如下图所示:
文字版如下:
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
可以看到,这里是按字节寻址,一个地址上面存放一个字节(1byte=8bit),即8位,用两个16进制表示,比如,在地址0x0000000
位置上,存放着CA
。在十六进制值中:
- C代表着十进制中的12,换算成二进制是
1100
- A代表着十进制中的10,换算成二进制是
1010
也就是说,地址0x0000000
位置上存放着11001010
。
下文我们将用很大篇幅详细分析上述文件。
类文件结构
Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据结构,无符号数和表:
- 无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节,8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的符合数据结构,为了便于区分,所有表的命名都习惯性地以
_info
结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作一张表。
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 |
详细分析
1. 魔数与Class文件版本号
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
结合类结构规定分析:
1 | u4 magic 1 |
Class文件开头首先是4个字节,代表魔数(magic number),唯一的作用就是确定这个文件是否是一个能被虚拟机接受的Class文件(类似文件拓展名)。
0x00000004
和0x00000005
两个字节,代表着次版本号(minor_version)。从JDK1.2以后,直到JDK12均为使用,全部固定为零。0x00000006
和0x00000007
两个字节,代表着主版本号(major_version)。Java的版本号从45开始,比如JDK1.1是45,JDK1.2是46,…,那么JDK11是55
,换算成十六进制是37H
,正好与上面字节对应。
这里有一个需要注意的地方,因为Class文件采用的是大端机存储模式,所以
00(低位)37(高位)
代表的是0037
,而不是3700
。
2.常量池
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
同样,我们来看一下类文件的格式规定:
1 | u2 constant_pool_count 1 |
地址0x00000008
和0x00000009
存储了constant_pool_count,值位1D
,换算为十进制为29
,代表存储了28
个常量,索引为[1-28]
。
如何查看常量池中的每一个常量,我们还需要 1 张表格(常量池的项目类型):
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | int类型字面值 |
CONSTANT_Float_info | 4 | float类型字面值 |
CONSTANT_Long_info | 5 | long类型字面值 |
CONSTANT_Double_info | 6 | double类型字面值 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | String类型字面值 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 表示方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
(1)第 1 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
0x0000000A
存储了0A
,对应着表中第10项CONSTANT_Methodref_info
。然后,再查阅对应的表格:
常量CONSTANT_Methodref_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为10 | |
index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 |
info代表这个常量是一个“表”结构,共有三项:
第二项是u2
,于是向后查看 2 个字节,是0006
,代表着指向第6个索引。
第三项是u2
,于是再向后查看 2 个字节,是000F
,代表着指向第 15 个索引。
(2)第 2 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x0000000F
位置存储了09
,查阅表格,发现是CONSTANT_Fieldref_info
类型。然后,查阅对应的表格:
常量CONSTANT_Fieldref_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为9 | |
index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 |
info代表这个常量是一个“表”结构,共有三项:
第二项是u2
,于是向后查看 2 个字节,是0010
,代表着指向第16个索引。
第三项是u2
,于是再向后查看 2 个字节,是0011
,代表着指向第 17 个索引。
(3)第 3 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x00000014
位置存储了08
,查阅表格,发现是CONSTANT_String_info
类型,说明第 2 个常量是字段的符号引用。然后,查阅对应的表格:
常量CONSTANT_String_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为8 | |
index | u2 | 指向字符串字面量的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0012
,代表着指向第18个索引。
(4)第 4 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x00000017
位置存储了0A
,查阅表格,发现是CONSTANT_Methodref_info
类型。然后,查阅对应的表格:
常量CONSTANT_Methodref_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为10 | |
index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 |
info代表这个常量是一个“表”结构,共有三项:
第二项是u2
,于是向后查看 2 个字节,是0013
,代表着指向第19个索引。
第三项是u2
,于是再向后查看 2 个字节,是0014
,代表着指向第 20 个索引。
(5)第 5 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x0000001C
位置存储了07
,查阅表格,发现是CONSTANT_Class_info
类型。然后,查阅对应的表格:
常量CONSTANT_Class_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为7 | |
index | u2 | 指向全限定名常量项的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0015
,代表着指向第21个索引。
(6)第 6 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x0000001F
位置存储了07
,查阅表格,发现是CONSTANT_Class_info
类型。然后,查阅对应的表格:
常量CONSTANT_Class_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为7 | |
index | u2 | 指向全限定名常量项的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0016
,代表着指向第22个索引。
(7)第 7 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x00000022
位置存储了01
,查阅表格,发现是CONSTANT_Utf8_info
类型。然后,查阅对应的表格:
常量CONSTANT_Utf8_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为1 | |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 |
info代表这个常量是一个“表”结构,共有3项:
第二项是u2
,于是向后查看 2 个字节,是0006
,代表着该字符串的长度为6个字节。
第三项是u1
,但是并不是向后查看1个字节,而是长度为length的字节。
查阅ASCII码转换表,可以得到字符串<init>
,说明第 7 个常量是字符串值。
1 | 3C696E69743E |
(8)第 8 个常量
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
继续向后看一个字节u1
,0x0000002B
位置存储了01
,查阅表格,发现是CONSTANT_Utf8_info
类型。然后,查阅对应的表格:
常量CONSTANT_Utf8_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为1 | |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 |
info代表这个常量是一个“表”结构,共有3项:
第二项是u2
,于是向后查看 2 个字节,是0003
,代表着该字符串的长度为3个字节。
第三项是u1
,但是并不是向后查看1个字节,而是长度为length的字节。
查阅ASCII码转换表,可以得到字符串()V
,说明第 8 个常量是字符串值。
1 | 282956 |
(9)其他常量……javap!
相信读者到这里已经理解Class文件常量的组织方式了,其他常量不再详细分析。
Oracle公司其实已经为我们准备好了一个专门用于分析Class文件字节码的工具:javap
。运行下面的代码:
1 | javap -v HelloWorld.class |
输出信息为:
1 | Classfile /C:/Users/chao/Desktop/Java_Perf/Demo/JVMSDemo/src/com/raining/HelloWorld.class |
其中,常量池为:
1 | Constant pool: |
可以看到,javap
已经帮我们把整个常量池中的28个常量都计算出来了,读者可以自行与我们之前计算的结果对比。
3.访问标志
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flag)
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
因为占用 2 个字节(2 * 8bit = 16位),所以一共有16个标志位可以使用,但是当前只定义了9个,没有用到的标志位要求一律为零。具体的标志位和含义如下:
标志名称 | 标志位 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的予以在JDK1.2发生过变化,为了区别,JDK1.0.2之后编译出来的类的这个标志必须为真 |
ACC_INTERFACE | 0x0200 | 标记这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户的代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
21
代表着这是一个ACC_PUBLIC
的普通类,使用了JDK1.2之后的编译器进行编译。这也可以从javac命令得到的输出中看出,互相验证。
1 | flags: (0x0021) ACC_PUBLIC, ACC_SUPER |
4.类索引、父类索引与接口索引集合
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
类型 | 名称 | 数量 |
---|---|---|
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
类索引(this_classs)和父类索引(super_class)都是一个u2
类型的数据,接口索引集合(interfaces)是一组u2
类型的数据的集合。
向后看 2 个字节u2
,0x00000144
和0x00000145
位置存储了0005
,查阅表格,发现是CONSTANT_Class_info
类型。最终,找到为:
1 | #5 = Class #21 // com/raining/HelloWorld |
向后看 2 个字节u2
,0x00000146
和0x00000147
位置存储了0006
,查阅表格,发现是CONSTANT_Class_info
类型。最终,找到为:
1 | #6 = Class #22 // java/lang/Object |
向后看 2 个字节u2
,0x00000148
和0x00000149
位置存储了0000
,说明接口数量为 0。
5.字段表集合
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | fields_count | 1 |
field_info | fields | fields_count |
对于每一个field,结构如下表所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
向后看 2 个字节u2
,0x0000014A
和0x0000014B
位置存储了0000
,说明fields_count=0,没有field。所以这里就不再展开。
6.方法表集合
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
方法表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | methods_count | 1 |
method_info | methods | methods_count |
Class文件存储格式中对method的描述与对字段的描述采用了几乎完全一致的方式。对于每一个method,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
向后看 2 个字节u2
,0x0000014C
和0x0000014D
位置存储了0002
,说明methods_count=2,存在两个method。
(1)第 1 个method
向后看 2 个字节u2
,0001
,对应access_flags,对应ACC_PUBLIC
。
向后看 2 个字节u2
,0007
,对应name_index,对应一个UTF-8字符串,为<init>
。
向后看 2 个字节u2
,0008
,对应descriptor_index,对应一个UTF-8字符串,为()V
。
向后看 2 个字节u2
,0001
,对应attributes_count,代表有 1 个attribute。
突然,我们发现进行不下去了,attribute_info这个表的结构是什么……,这里插入讲解一下,注意保持住思路,别被突如其来的表格打乱。
attribute_info表的结构如下所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | info | attribute_length |
其中Code属性的结构表:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_loacls | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
好了,有了表结构,我们就知道这么取值了。
向后看 2 个字节u2
,0009
,对应attribute_name_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为Code
,它代表了该属性的属性名称。
根据Code属性的结构表:
向后看 4 个字节u4
,00 00 00 1D
,对应attribute_length,长度为29,如下所示。
1 | 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03 |
向后看 2 个字节u2
,0001
,对应max_stack,值为1
向后看 2 个字节u2
,0001
,对应max_loacls,值为1
向后看 4 个字节u4
,00 00 00 05
,对应code_length,值为5
向后看 5 个字节code_length
,这就是所谓的字节码!!!
1 | 2A B7 00 01 B1 |
向后看 2 个字节u2
,0000
,对应exception_table_length,值为0,说明没有exception_table
向后看 2 个字节u2
,0001
,对应attributes_count,值为1,说明有一个attribute。
接下来又是一个attribute:
向后看 2 个字节u2
,000A
,对应attribute_name_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为LineNumberTable
,它代表了该属性的属性名称。
LineNumberTable 表的结构如下所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
其中,line_number_info表包含start_pc和line_number两个u2类型的数据项前者是字节码行号,后者是Java源码行号。
向后看 4 个字节u4
,00000006
,对应attribute_length,值为6。
向后看 2 个字节u2
,0001
,对应line_number_table_length,值为1。
接下来就是一项line_number_info:
- 向后看 2 个字节
u2
,0000
,对应start_pc,字节码行号 - 向后看 2 个字节
u2
,0003
,对应line_number,Java源码行号
总结一下:
1 | //第一个方法 |
(2)第 2 个method
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
再将每个method的结构表贴一遍:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
向后看 2 个字节u2
,0009
,对应access_flags,对应ACC_PUBLIC
和ACC_STATIC
。
向后看 2 个字节u2
,000B
,对应name_index,对应一个UTF-8字符串,为main
。
向后看 2 个字节u2
,000C
,对应descriptor_index,对应一个UTF-8字符串,为([Ljava/lang/String;)V
。
向后看 2 个字节u2
,0001
,对应attributes_count,代表有 1 个attribute。
接下来分析这个attribute:
向后看 2 个字节u2
,0009
,对应attribute_name_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为Code
,它代表了该属性的属性名称。
根据Code属性的结构表:
向后看 4 个字节u4
,00 00 00 25
,对应attribute_length,长度为37(十进制),如下所示。
1 | 00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00 |
向后看 2 个字节u2
,0002
,对应max_stack,值为2
向后看 2 个字节u2
,0001
,对应max_loacls,值为1
向后看 4 个字节u4
,00 00 00 09
,对应code_length,值为9
向后看 9 个字节code_length
,字节码!!!
1 | B2 00 02 12 03 B6 00 04 B1 |
向后看 2 个字节u2
,0000
,对应exception_table_length,值为0,说明没有exception_table
向后看 2 个字节u2
,0001
,对应attributes_count,值为1,说明有一个attribute。
接下来分析这个attribute:
向后看 2 个字节u2
,000A
,对应attribute_name_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为LineNumberTable
。
接下来分析这个LineNumberTable:
向后看 4 个字节u4
,0000000A
,对应attribute_length,值为10。
向后看 2 个字节u2
,0002
,对应line_number_table_length,值为2。
接下来就是 2 项line_number_info:
- 第一项:
- 向后看 2 个字节
u2
,0000
,对应start_pc,字节码行号 - 向后看 2 个字节
u2
,0005
,对应line_number,Java源码行号
- 向后看 2 个字节
- 第二项:
- 向后看 2 个字节
u2
,0008
,对应start_pc,字节码行号 - 向后看 2 个字节
u2
,0006
,对应line_number,Java源码行号
- 向后看 2 个字节
总结一下:
1 | //第 2 个方法 |
7.属性表集合
大家绕了那么久,不要忘记我们还有最后一项属性表集合,前文所有的attribute_info都是子属性!
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
1 | Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
向后看 2 个字节u2
,0001
,对应attributes_count,值为1,说明有一个attribute。
接下来分析这个attribute:
向后看 2 个字节u2
,000D
,对应attribute_name_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为SourceFile
。
SourceFile的属性结构表如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
向后看 4 个字节u4
,00000002
,对应attribute_length,值为2,说明这个attribute长度为2。
所以向后看 2 个字节,000E
,对应sourcefile_index,它是一项指向CONSTANT_Utf-8_info型常量的索引,查表为HelloWorld.java
。
总结一下:
1 | SourceFile: HelloWorld.java |
字节码指令
<init>
函数
我们先来看一下<init
函数的字节码指令:
1 | Code: |
查表翻译得到:
1 | aload_0 //将第1个引用类型本地变量推送到栈顶 |
main
函数
main
方法的字节码指令:
1 | Code: |
查表翻译得到:
1 | getstatic //获取指定类的静态域,并将其值压入栈顶 |
由于本文的主旨是分析类文件结构,而不是专门探究字节码指令,所以不详细展开,读者若希望深入分析,可以自行查阅相关资料。
参考
- 《深入理解Java虚拟机》,周志明。