Java类文件结构——以HelloWorld为例逐字节分析
本文旨在通过对HelloWorld
代码编译后的类文件进行逐字节分析,讲解Class类文件的结构。
准备工作
先准备一段 Java 代码
package com.raining;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello,World!");
}
}
然后,用 javac
编译,得到class
文件
class
文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中。注意是**大端机(高位在前)**的存储方式。
准备工具WinHex,WinHex可以轻松的以十六进制的格式打开文本文件,方便我们查看class
文件中的二进制码。使用WinHex打开class文件,界面如下图所示:
文字版如下:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
00000020 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29
00000030 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E
00000040 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69
00000050 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67
00000060 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75
00000070 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57
00000080 6F 72 6C 64 2E 6A 61 76 61 0C 00 07 00 08 07 00
00000090 17 0C 00 18 00 19 01 00 0C 48 65 6C 6C 6F 2C 57
000000A0 6F 72 6C 64 21 07 00 1A 0C 00 1B 00 1C 01 00 16
000000B0 63 6F 6D 2F 72 61 69 6E 69 6E 67 2F 48 65 6C 6C
000000C0 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F 6C 61
000000D0 6E 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61
000000E0 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F
000000F0 75 74 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72
00000100 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76
00000110 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
00000120 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A
00000130 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
00000140 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00000150 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01
00000160 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00
00000170 00 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
可以看到,这里是按字节寻址,一个地址上面存放一个字节(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文件版本号
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
结合类结构规定分析:
u4 magic 1
u2 minor_version 1
u2 major_version 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.常量池
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
同样,我们来看一下类文件的格式规定:
u2 constant_pool_count 1
cp_info constant_pool 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 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
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 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
继续向后看一个字节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 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
继续向后看一个字节u1
,0x00000014
位置存储了08
,查阅表格,发现是CONSTANT_String_info
类型,说明第 2 个常量是字段的符号引用。然后,查阅对应的表格:
常量CONSTANT_String_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为8 | |
index | u2 | 指向字符串字面量的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0012
,代表着指向第18个索引。
(4)第 4 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
继续向后看一个字节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 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
00000020 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29
继续向后看一个字节u1
,0x0000001C
位置存储了07
,查阅表格,发现是CONSTANT_Class_info
类型。然后,查阅对应的表格:
常量CONSTANT_Class_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为7 | |
index | u2 | 指向全限定名常量项的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0015
,代表着指向第21个索引。
(6)第 6 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
00000020 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29
继续向后看一个字节u1
,0x0000001F
位置存储了07
,查阅表格,发现是CONSTANT_Class_info
类型。然后,查阅对应的表格:
常量CONSTANT_Class_info | 项目 | 类型 | 描述 |
---|---|---|---|
tag | u1 | 值为7 | |
index | u2 | 指向全限定名常量项的索引 |
info代表这个常量是一个“表”结构,共有2项:
第二项是u2
,于是向后查看 2 个字节,是0016
,代表着指向第22个索引。
(7)第 7 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000000 CA FE BA BE 00 00 00 37 00 1D 0A 00 06 00 0F 09
00000010 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
00000020 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29
继续向后看一个字节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码转换表,可以得到字符串,说明第 7 个常量是字符串值。
3C696E69743E
(8)第 8 个常量
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000020 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29
00000030 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E
00000040 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69
继续向后看一个字节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 个常量是字符串值。
282956
()V
(9)其他常量......javap!
相信读者到这里已经理解Class文件常量的组织方式了,其他常量不再详细分析。
Oracle公司其实已经为我们准备好了一个专门用于分析Class文件字节码的工具:javap
。运行下面的代码:
javap -v HelloWorld.class
输出信息为:
Classfile /C:/Users/chao/Desktop/Java_Perf/Demo/JVMSDemo/src/com/raining/HelloWorld.class
Last modified 2023年8月10日; size 438 bytes
MD5 checksum 7afa532cba2da0dfb9903ff3dcfbb06f
Compiled from "HelloWorld.java"
public class com.raining.HelloWorld
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // com/raining/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello,World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/raining/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello,World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/raining/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public com.raining.HelloWorld();
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 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello,World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "HelloWorld.java"
其中,常量池为:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello,World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // com/raining/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello,World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 com/raining/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
可以看到,javap
已经帮我们把整个常量池中的28个常量都计算出来了,读者可以自行与我们之前计算的结果对比。
3.访问标志
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000140 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00000150 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01
00000160 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00
00000170 00 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
在常量池结束之后,紧接着的 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命令得到的输出中看出,互相验证。
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
4.类索引、父类索引与接口索引集合
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000140 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00000150 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01
00000160 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00
00000170 00 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
类型 | 名称 | 数量 |
---|---|---|
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
类型。最终,找到为:
#5 = Class #21 // com/raining/HelloWorld
向后看 2 个字节u2
,0x00000146
和0x00000147
位置存储了0006
,查阅表格,发现是CONSTANT_Class_info
类型。最终,找到为:
#6 = Class #22 // java/lang/Object
向后看 2 个字节u2
,0x00000148
和0x00000149
位置存储了0000
,说明接口数量为 0。
5.字段表集合
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000140 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00000150 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01
00000160 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00
00000170 00 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
字段表结构:
类型 | 名称 | 数量 |
---|---|---|
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.方法表集合
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000140 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
00000150 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01
00000160 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00
00000170 00 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
方法表结构:
类型 | 名称 | 数量 |
---|---|---|
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字符串,为。
向后看 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,如下所示。
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
,这就是所谓的字节码!!!
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源码行号
总结一下:
//第一个方法
ACC_PUBLIC
()V
Code:
max_stack=1
max_loacls=1
code_length=5
code:
2A B7 00 01 B1 //字节码指令
LineNumberTable:
0:3
(2)第 2 个method
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
00000170 00 00 06 00 01 00 00 00 03 *00 09 00 0B 00 0C 00
00000180 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2
00000190 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D
000001B0 00 00 00 02 00 0E
再将每个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(十进制),如下所示。
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
00 0A 00 02 00 00 00 05 00 08 00 06
向后看 2 个字节u2
,0002
,对应max_stack,值为2
向后看 2 个字节u2
,0001
,对应max_loacls,值为1
向后看 4 个字节u4
,00 00 00 09
,对应code_length,值为9
向后看 9 个字节code_length
,字节码!!!
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 个字节
总结一下:
//第 2 个方法
ACC_PUBLIC ACC_STATIC
main ([Ljava/lang/String;)V
Code:
max_stack=2
max_loacls=1
code_length=9
code:
B2 00 02 12 03 B6 00 04 B1 //字节码指令
LineNumberTable:
0:5
8:6
7.属性表集合
大家绕了那么久,不要忘记我们还有最后一项属性表集合,前文所有的attribute_info都是子属性!
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_count | 1 |
attribute_info | attributes | attributes_count |
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
000001A0 00 0A 00 02 00 00 00 05 00 08 00 06 *00 01 00 0D
000001B0 00 00 00 02 00 0E
向后看 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
。
总结一下:
SourceFile: HelloWorld.java
字节码指令
函数
我们先来看一下