深入class文件结构

JVM指令集简介

首先将下面这段java代码对应编译成的class文件转化为Oolong语言后进行分析:

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

得到的.j后缀文件内容如下:

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
28
.source Message.java
.class public super org/example/test/Message
.super java/lang/Object


.method public <init> ()V
.limit stack 1
.limit locals 1
.var 0 is this Lorg/example/test/Message; from l0 to l5
.line 3
l0: aload_0
l1: invokespecial java/lang/Object/<init> ()V
l4: return

.end method

.method public static main ([Ljava/lang/String;)V
.limit stack 2
.limit locals 1
.var 0 is args [Ljava/lang/String; from l0 to l9
.line 5
l0: getstatic java/lang/System/out Ljava/io/PrintStream;
l3: ldc "Hello World!"
l5: invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V
.line 6
l8: return

.end method

与类相关的指令

.source Message.java:表示这个代码的源文件是Message.java

.class public super org/example/test/Message:表示这个是公共类Message

.super java/lang/Object:表示这个类的父类是Object

方法的定义

.method public ()V:表示这是一个公共方法,没有参数,返回值类型是V(Void),表示是构造函数

.method public static main ([Ljava/lang/String;)V:表示定义一个main方法,参数是String类型的数组,L表示的是一个类形式,凡是L表示的类后面都会以;结尾,表示这个类的结束

其余指令解释

  1. .limit stack 1:设置该方法的操作数栈的最大深度为1。
  2. .limit locals 1:设置该方法的局部变量表的最大槽位数为1。
  3. .var 0 is this Lorg/example/test/Message; from l0 to l5:定义一个局部变量,槽位为0,名称为this,类型为Lorg/example/test/Message;,作用范围从标签l0到标签l5
  4. .line 3:指定源代码行号为3。
  5. l0: aload_0:将局部变量0(即this)加载到操作数栈中。
  6. l1: invokespecial java/lang/Object/<init> ()V:调用父类java/lang/Object的构造函数。
  7. l4: return:从构造函数中返回。
  8. .end method:结束方法定义。
  9. .limit stack 2:设置该方法的操作数栈的最大深度为2。
  10. .limit locals 1:设置该方法的局部变量表的最大槽位数为1。
  11. .var 0 is args [Ljava/lang/String; from l0 to l9:定义一个局部变量,槽位为0,名称为args,类型为[Ljava/lang/String;,作用范围从标签l0到标签l9
  12. .line 5:指定源代码行号为5。
  13. l0: getstatic java/lang/System/out Ljava/io/PrintStream;:获取System.out静态字段的值(即标准输出流)。
  14. l3: ldc "Hello World!":将字符串常量”Hello World!”加载到操作数栈中。
  15. l5: invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V:调用PrintStream类的println方法,将操作数栈顶的字符串打印到标准输出。
  16. .line 6:指定源代码行号为6。
  17. l8: return:从main方法返回。
  18. .end method:结束方法定义。

class文件头的表示形式

由于篇幅过长,这个二进制字节码将在后续的分析中拆分展示

使用java COM.sootNsmoke.oolong.Dumpclass Message.class命令,可以生成有字节码解释形式的内容,其中实际上只有第二列是原始的字节码内容,第一列和第三列都是生成的解释性内容

首先看一下文件的头部信息,有三行:

1
2
3
000000 cafebabe          magic = ca fe ba be
000004 0000 minor version = 0
000006 0034 major version = 52

第一行是一个标识符,表示这个文件是一个标准的class文件,它是一个32位的无符号整数,cafebabe是这个整数的16进制表示形式,如果一个文件的前4个字节是这个数字,则表示这个文件是一个class文件,否则JVM将不会加载

后面两个字节分别表示最大版本和最小的版本范围

常量池

文件头信息后面是常量池的内容:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
000008 0022              34 constants
00000a 0a00060014 1. Methodref class #6 name-and-type #20
00000f 0900150016 2. Fieldref class #21 name-and-type #22
000014 080017 3. String #23
000017 0a00180019 4. Methodref class #24 name-and-type #25
00001c 07001a 5. Class name #26
00001f 07001b 6. Class name #27
000022 010006 7. UTF length=6
000025 3c696e69743e <init>
00002b 010003 8. UTF length=3
00002e 282956 ()V
000031 010004 9. UTF length=4
000034 436f6465 Code
000038 01000f 10. UTF length=15
00003b 4c696e654e756d6265725461626c65 LineNumberTable
00004a 010012 11. UTF length=18
00004d 4c6f63616c5661726961626c65546162 LocalVariableTab
00005d 6c65 le
00005f 010004 12. UTF length=4
000062 74686973 this
000066 01001a 13. UTF length=26
000069 4c6f72672f6578616d706c652f746573 Lorg/example/tes
000079 742f4d6573736167653b t/Message;
000083 010004 14. UTF length=4
000086 6d61696e main
00008a 010016 15. UTF length=22
00008d 285b4c6a6176612f6c616e672f537472 ([Ljava/lang/Str
00009d 696e673b2956 ing;)V
0000a3 010004 16. UTF length=4
0000a6 61726773 args
0000aa 010013 17. UTF length=19
0000ad 5b4c6a6176612f6c616e672f53747269 [Ljava/lang/Stri
0000bd 6e673b ng;
0000c0 01000a 18. UTF length=10
0000c3 536f7572636546696c65 SourceFile
0000cd 01000c 19. UTF length=12
0000d0 4d6573736167652e6a617661 Message.java
0000dc 0c00070008 20. NameAndType name #7 descriptor #8
0000e1 07001c 21. Class name #28
0000e4 0c001d001e 22. NameAndType name #29 descriptor #30
0000e9 01000c 23. UTF length=12
0000ec 48656c6c6f20576f726c6421 Hello World!
0000f8 07001f 24. Class name #31
0000fb 0c00200021 25. NameAndType name #32 descriptor #33
000100 010018 26. UTF length=24
000103 6f72672f6578616d706c652f74657374 org/example/test
000113 2f4d657373616765 /Message
00011b 010010 27. UTF length=16
00011e 6a6176612f6c616e672f4f626a656374 java/lang/Object
00012e 010010 28. UTF length=16
000131 6a6176612f6c616e672f53797374656d java/lang/System
000141 010003 29. UTF length=3
000144 6f7574 out
000147 010015 30. UTF length=21
00014a 4c6a6176612f696f2f5072696e745374 Ljava/io/PrintSt
00015a 7265616d3b ream;
00015f 010013 31. UTF length=19
000162 6a6176612f696f2f5072696e74537472 java/io/PrintStr
000172 65616d eam
000175 010007 32. UTF length=7
000178 7072696e746c6e println
00017f 010015 33. UTF length=21
000182 284c6a6176612f6c616e672f53747269 (Ljava/lang/Stri
000192 6e673b2956 ng;)V

第一行有两个字节表示的是在该类中含有的常量总数总共有34个,含有一个0是保留常量

这个34个常量都是由三个字节来描述的,分别是一个字节的常量类型和两个字节的常量池索引地址

UTF8常量类型

UTF8是一种字符编码的格式,可以存储多个字节长度的字符串值,如可以存储类名或者方法名等很长的一个字符串

从左向右看,UTF8类型的常量最后两个字节来表示后面所存储的字符串的总字节数,如第32个常量:010007(十六进制),其中0007表示后面所跟的常量实际内容的字节长度为7,即总共由7个字节:7072696e746c6e(十六进制字符串),表示的实际内容是:println

第一个字节01表示UTF8常量类型对应的类型码

因此,UTF8类型的常量格式如下:

一个字节表示的UTF8常量类型 + 两个字节表示的该常量字节长度 + 该常量的具体内容

Fieldref、Methodref常量类型

这两个常量类型是为了描述Class中的属性项和方法

如常量2:0900150016,就是一个Fieldref类型的常量,总共五个字节

  1. 第一个字节 09 表示Fieldref常量的类型
  2. 第二个和第三个字节表示该Fieldref是哪个类中的Field,存储的值是第几个常量的位置,0015表示第21个常量(Class类型常量)
  3. 最后两个字节表示这个Fieldref常量的Name和Type,指向NameAndType类型常量的索引,即0016表示的第22个常量

Methodref类常量类似,如常量1:0a00060014

  1. 第一个字节0a表示常量类型10
  2. 第二个和第三个字节表示该方法属于哪个类,指向特定的常量
  3. 最后两个字节表示该方法的NameAndType

Class常量类型

该常量表示该类的名称,会指向另一个UTF8类型的常量,这个UTF8类型常量存储了这个类具体的名称

如常量5:07001a:

  1. 第一个字节表示Class常量的类型
  2. 后两个字节表示指向第26个常量,该常量是UTF8类型,常量为010018,存储的是org/example/test/Message,即该类的名称

NameAndType常量类型

如常量25:0c00200021:

  1. 第一个字节表示NameAndType类型的常量
  2. 后面两个字节表示指向Name的UTF8类型常量索引,0020表示第32个常量println,表示属性项名称(对于Fieldref)或者方法名称(对于Methodref)
  3. 最后两个字节指向Type的UTF8类型常量索引,0021表示第33个常量(Ljava/lang/String;)V,表示参数类型(对于Fieldref)或者返回类型(对于Methodref)

类信息

常量列表后面是关于这个类本身信息的描述,例如这个类的访问控制、名称和类型,以及是否由父类或者是否实现了某些接口等信息

如:

1
2
3
4
000197 0021              access_flags = 33
000199 0005 this = #5
00019b 0006 super = #6
00019d 0000 0 interfaces

0021两个字节表示这个类的访问控制描述

实际上这两个字节只用了5个bit,其它的都是0(没有定义),将0021翻译成二进制:0000 0000 0010 0001,加粗的为真正用到的bit位(从右到左)

  1. 第1个bit表示该类是否是public的,为1是public类型,否则为private,因此类的访问修饰只有两种
  2. 第5个bit表示该类是否是final,1为是,0为否
  3. 第6个bit表示该类是否含有invokespecial,也就是是否继承其他类,所有类都默认继承了Object
  4. 第10个bit表示该类是否是抽象类,0为不是,1为是

0005两个字节是该类的名称,指向第5个常量

0006两个字节表示该类的父类名称,指向第6个常量

0000两个字节表示该类没有实现接口

Fields和Methods定义

类信息描述后面是每个Fields和Methods的具体定义

如:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
00019f 0000              0 fields
0001a1 0002 2 methods
Method 0:
0001a3 0001 access flags = 1
0001a5 0007 name = #7<<init>>
0001a7 0008 descriptor = #8<()V>
0001a9 0001 1 field/method attributes:
field/method attribute 0
0001ab 0009 name = #9<Code>
0001ad 0000002f length = 47
0001b1 0001 max stack: 1
0001b3 0001 max locals: 1
0001b5 00000005 code length: 5
0001b9 2a 0 aload_0
0001ba b70001 1 invokespecial #1
0001bd b1 4 return
0001be 0000 0 exception table entries:
0001c0 0002 2 code attributes:
code attribute 0:
0001c2 000a name = #10<LineNumberTable>
0001c4 00000006 length = 6
Line number table:
0001c8 0001 length = 1
0001ca 00000003 start pc: 0 line number: 3
code attribute 1:
0001ce 000b name = #11<LocalVariableTable>
0001d0 0000000a length = 10
0001d4 0001 local_variable_table length: 1
0001d6 0000 start pc: 0
0001d8 0005 length: 5
0001da 000c name_index: 12
0001dc 000d descriptor_index: 13
0001de 0000 index: 0
Method 1:
0001e0 0009 access flags = 9
0001e2 000e name = #14<main>
0001e4 000f descriptor = #15<([Ljava/lang/String;)V>
0001e6 0001 1 field/method attributes:
field/method attribute 0
0001e8 0009 name = #9<Code>
0001ea 00000037 length = 55
0001ee 0002 max stack: 2
0001f0 0001 max locals: 1
0001f2 00000009 code length: 9
0001f6 b20002 0 getstatic #2
0001f9 1203 3 ldc #3
0001fb b60004 5 invokevirtual #4
0001fe b1 8 return
0001ff 0000 0 exception table entries:
000201 0002 2 code attributes:
code attribute 0:
000203 000a name = #10<LineNumberTable>
000205 0000000a length = 10
Line number table:
000209 0002 length = 2
00020b 00000005 start pc: 0 line number: 5
00020f 00080006 start pc: 8 line number: 6
code attribute 1:
000213 000b name = #11<LocalVariableTable>
000215 0000000a length = 10
000219 0001 local_variable_table length: 1
00021b 0000 start pc: 0
00021d 0009 length: 9
00021f 0010 name_index: 16
000221 0011 descriptor_index: 17
000223 0000 index: 0

前四个字节分别表示在该类中定义了多少个属性和方法,后面是这些属性和方法的具体定义

方法和属性与类应用也有访问控制,具体定义的前两个字节即定义了访问控制:0000 x00x xxxx xxxx

  1. 第1个bit表示是否public
  2. 第2个bit表示是否private
  3. 第3个bit表示是否protected
  4. 第4个bit表示是否static
  5. 第5个bit表示有没有被final定义
  6. 第6个bit表示是否被synchronized定义
  7. 第7个bit表示是否被volatile定义
  8. 第8个bit表示是否被transient定义
  9. 第9个bit表示是否native方法
  10. 第12个bit表示是否abstract方法

后面四个字节0007 0008定义了这个方法的NameAndType,分别指向两个UTF8常量类型

0000002f表示这个方法的代码长度,编译后的字节码长度是47个字节,这里表示长度用了四个字节,按照这个长度的定义来说是最长可以2^32个字节,也就是4GB的长度,但是实际上整个Jvaa源码长度只有64K的字节长度可以表示。这里的64K长度不是源码的方法长度,而是编译后的字节码长度

后面还描述了这个方法的最大栈深度以及本地常量的最大个数、方法定义抛出的异常等信息

最后是方法的附加信息描述,描述了代码本身之外的额外信息,如调试信息等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0001c0 0002                    2 code attributes:
code attribute 0:
0001c2 000a name = #10<LineNumberTable>
0001c4 00000006 length = 6
Line number table:
0001c8 0001 length = 1
0001ca 00000003 start pc: 0 line number: 3
code attribute 1:
0001ce 000b name = #11<LocalVariableTable>
0001d0 0000000a length = 10
0001d4 0001 local_variable_table length: 1
0001d6 0000 start pc: 0
0001d8 0005 length: 5
0001da 000c name_index: 12
0001dc 000d descriptor_index: 13
0001de 0000 index: 0

类属性描述

字节码的最后是Class的附加属性描述:

1
2
3
4
5
000225 0001              1 classfile attributes
Attribute 0:
000227 0012 name = #18<SourceFile>
000229 00000002 length = 2
00022d 0013 sourcefile index = #19