Java Class 文件结构

看class文件结构最直接的方法应该看The Java ® Virtual Machine Specification Java SE 8 Edition

第四章中有关于class文件格式的介绍。

Class文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
  • Class文件是有8个字节为基础的字节流构成的,这些字节流之间都严格按照规定的顺序排列,并且字节之间不存在任何空隙。
  • Class文件结构采用类似C语言的结构体来存储数据的,主要有两类数据项,无符号数和表,无符号数用来表述数字,索引引用以及字符串等。
  • u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数。
  • 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的该数据项的形式,称这一系列连续的摸一个类型的数据为某一类型的集合,比如,fields_count 个 field_info 表数据构成了字段表集合。

以实际文件为例

写一个hello world的java类

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("hello world!");
}
}

编译

1
javac Main.java

成生Main.class文件

用二进制文件文件查看

1
hexdump -C Main.class

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
00000000  ca fe ba be 00 00 00 34  00 1d 0a 00 06 00 0f 09  |.......4........|
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 |.....<init>...()|
00000030 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e |V...Code...LineN|
00000040 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69 |umberTable...mai|
00000050 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 |n...([Ljava/lang|
00000060 2f 53 74 72 69 6e 67 3b 29 56 01 00 0a 53 6f 75 |/String;)V...Sou|
00000070 72 63 65 46 69 6c 65 01 00 09 4d 61 69 6e 2e 6a |rceFile...Main.j|
00000080 61 76 61 0c 00 07 00 08 07 00 17 0c 00 18 00 19 |ava.............|
00000090 01 00 0c 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 07 |...hello world!.|
000000a0 00 1a 0c 00 1b 00 1c 01 00 04 4d 61 69 6e 01 00 |..........Main..|
000000b0 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 |.java/lang/Objec|
000000c0 74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 |t...java/lang/Sy|
000000d0 73 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 |stem...out...Lja|
000000e0 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 |va/io/PrintStrea|
000000f0 6d 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 |m;...java/io/Pri|
00000100 6e 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 |ntStream...print|
00000110 6c 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 |ln...(Ljava/lang|
00000120 2f 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 |/String;)V.!....|
00000130 00 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 |................|
00000140 00 00 00 1d 00 01 00 01 00 00 00 05 2a b7 00 01 |............*...|
00000150 b1 00 00 00 01 00 0a 00 00 00 06 00 01 00 00 00 |................|
00000160 01 00 09 00 0b 00 0c 00 01 00 09 00 00 00 25 00 |..............%.|
00000170 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04 b1 |................|
00000180 00 00 00 01 00 0a 00 00 00 0a 00 02 00 00 00 03 |................|
00000190 00 08 00 04 00 01 00 0d 00 00 00 02 00 0e |..............|
0000019e

幻数(magic)

前四个字节为固定的幻数0xCAFEBABE

版本号

接下来的四个字节为次版本号和主版本号。
上图版本号为52.0。(0x34=52)
也是版本号与jdk的对应关系。

1
2
3
4
5
6
7
8
J2SE 8 = 52,
J2SE 7 = 51,
J2SE 6.0 = 50,
J2SE 5.0 = 49,
JDK 1.4 = 48,
JDK 1.3 = 47,
JDK 1.2 = 46,
JDK 1.1 = 45

常量池

0x001d = 29 常量池长度为29

cp_info constant_pool[constant_pool_count-1];
在这个数组中,每个
constant_pool表中的每个项目必须以指示的1字节标签开头一种cp_info结构。cp_info数组的内容随标签的值而变化。有效标签及其值列见表。 每个标签字节必须是之后是两个或多个字节,提供有关特定常数的信息。该
附加信息的格式随标签值而异。

1
2
3
4
cp_info {
u1 tag;
u1 info[];
}

Constant pool tags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

每个类型,后面的字节数不一样

1
2
3
4
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}

access_flags

表示类或者接口的访问信息

Class access and property modifiers

1
2
3
4
5
6
7
8
9
10
11
Flag Name Value Interpretation
ACC_PUBLIC 0x0001 Declared public; may be accessed from outside its
package.
ACC_FINAL 0x0010 Declared final; no subclasses allowed.
ACC_SUPER 0x0020 Treat superclass methods specially when invoked by
the invokespecial instruction.
ACC_INTERFACE 0x0200 Is an interface, not a class.
ACC_ABSTRACT 0x0400 Declared abstract; must not be instantiated.
ACC_SYNTHETIC 0x1000 Declared synthetic; not present in the source code.
ACC_ANNOTATION 0x2000 Declared as an annotation type.
ACC_ENUM 0x4000 Declared as an enum type.

this_class

表示类的常量池索引,指向常量池中CONSTANT_Class_info的常量

super_class

表示超类的索引,指向常量池中CONSTANT_Class_info的常量

interface_counts及interface[interface_counts]

表示接口的数量及它里面每一项都指向常量池中CONSTANT_Class_info常量

fields_count 及 field_info fields[fields_count]

表示类的实例变量和类变量的数量及字段表的信息。

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

methods_count 及 method_info methods[methods_count]

方法表的数量及方法列表

1
2
3
4
5
6
7
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

attributes_count 及 attribute_info attributes[attributes_count]

表示属性表的数量,说到属性表

1
2
3
4
5
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

属性表存在于Class文件结构的最后,字段表,方法表以及Code属性中。

格式检查

当Java虚拟机加载预期类文件时,Java虚拟机首先确保文件具有类文件的基本格式。此过程称为格式检查。检查如下:

  • 前四个字节必须包含正确的幻术数字
  • 所有识别的属性必须具有适当的长度
  • 类文件不能被截断,或者最后有额外的字节
  • 常量池必须满足上面记录的限制。
  • 常量池中的所有字段引用和方法引用必须有有效名称,有效类和有效描述符

格式检查不能确保给定的字段或方法实际存在给定的类中,给定的描述符是指实际类。格式检查确保只有这些项目形成良好。更详细的检查当字节码本身被验证并且在解析期间被执行。
这些对基本类文件完整性的检查对于任何解释都是必要的类文件内容。格式检查不同于字节码验证,
虽然历史上他们都是混淆的,因为两者都是一种诚信的形式检查。

解析

1
javap -verbose Main

这样可以看到常量池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()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 // Main
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Main.java
#15 = NameAndType #7:#8 // "<init>":()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 Main
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System

后面

这样看来java class 文件格式并不复杂,以C结构体的方式组织,层次清晰。但因为时间关系,未把结构的细节都介绍完,比如常量池中常用类型的长度及引用规则。后面再通过分析一个复杂点的java代码进行补上。如果有时间,可以试着写一个class文件格式验证器。

然后,找了找。有这样想法的人也很多。在github上找到了几个。
比如:

参考