Hello World

我们来看看 Java 的 Hello World 程序经过 Javac (Java Compiler) 之后,会编译成 Java 字节码文件。

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
 Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 3B 00 1D 0A 00 02 00 03 07 J~:>...;........
00000010: 00 04 0C 00 05 00 06 01 00 10 6A 61 76 61 2F 6C ..........java/l
00000020: 61 6E 67 2F 4F 62 6A 65 63 74 01 00 06 3C 69 6E ang/Object...<in
00000030: 69 74 3E 01 00 03 28 29 56 09 00 08 00 09 07 00 it>...()V.......
00000040: 0A 0C 00 0B 00 0C 01 00 10 6A 61 76 61 2F 6C 61 .........java/la
00000050: 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74 01 ng/System...out.
00000060: 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 ..Ljava/io/Print
00000070: 53 74 72 65 61 6D 3B 08 00 0E 01 00 0C 48 65 6C Stream;......Hel
00000080: 6C 6F 20 57 6F 72 6C 64 21 0A 00 10 00 11 07 00 lo.World!.......
00000090: 12 0C 00 13 00 14 01 00 13 6A 61 76 61 2F 69 6F .........java/io
000000a0: 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 00 07 70 /PrintStream...p
000000b0: 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 61 2F rintln...(Ljava/
000000c0: 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 07 00 lang/String;)V..
000000d0: 16 01 00 18 4A 61 76 61 2F 56 53 63 6F 64 65 50 ....Java/VScodeP
000000e0: 72 6F 6A 65 63 74 2F 48 65 6C 6C 6F 01 00 04 43 roject/Hello...C
000000f0: 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 ode...LineNumber
00000100: 54 61 62 6C 65 01 00 04 6D 61 69 6E 01 00 16 28 Table...main...(
00000110: 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 [Ljava/lang/Stri
00000120: 6E 67 3B 29 56 01 00 0A 53 6F 75 72 63 65 46 69 ng;)V...SourceFi
00000130: 6C 65 01 00 0A 48 65 6C 6C 6F 2E 6A 61 76 61 00 le...Hello.java.
00000140: 21 00 15 00 02 00 00 00 00 00 02 00 01 00 05 00 !...............
00000150: 06 00 01 00 17 00 00 00 1D 00 01 00 01 00 00 00 ................
00000160: 05 2A B7 00 01 B1 00 00 00 01 00 18 00 00 00 06 .*7..1..........
00000170: 00 01 00 00 00 03 00 09 00 19 00 1A 00 01 00 17 ................
00000180: 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 07 12 ...%........2...
00000190: 0D B6 00 0F B1 00 00 00 01 00 18 00 00 00 0A 00 .6..1...........
000001a0: 02 00 00 00 05 00 08 00 06 00 01 00 1B 00 00 00 ................
000001b0: 02 00 1C ...

然后我们可以使用 Java 自带的反编译工具 javap 来对字节码反编译:

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
Classfile /E:/Work/Java/VScodeProject/Hello.class
Last modified 2021年8月14日; size 435 bytes
SHA-256 checksum 324a5e74b6109dc3e1235281f57ee39e74d08af19760dda79433bd878f5a7ece
Compiled from "Hello.java"
public class Java.VScodeProject.Hello
minor version: 0
major version: 59
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // Java/VScodeProject/Hello
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // Hello Wrold!
#14 = Utf8 Hello Wrold!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // Java/VScodeProject/Hello
#22 = Utf8 Java/VScodeProject/Hello
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 Hello.java
{
public Java.VScodeProject.Hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()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 #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello Wrold!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "Hello.java"

对于一个字节码文件来说,一共由以下十个部分的组成:

  • 魔数字 Magic Number 类似于网络传输的 MIME 协议,用来标识文件的内容类型。

  • 版本号 Minor/Major Version 生成的字节码 Javac 版本。

  • 常量池 Constant Pool

  • 类访问标记 Access Flag

  • 类访问索引 This Class

  • 超类索引 Super Class

  • 接口表索引 Interface

  • 字段表 Field

  • 方法表 Method

  • 属性表 Attribute

魔数 Magic Number

很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)。因此这几个字节的内容也被称为魔数 (Magic Number),因为根据这几个字节的内容就可以确定文件类型。

Java 字节码文件 .class 的内容开头标识为前四个字节: CAFEBABE,咖啡宝贝:

328.jpg

当我们修改了 .class 里面的魔数之后,JVM 再去加载并运行这个类的时候,就会报错了,说魔数值不相容 Incompatible:

1
2
3
E:\Work>java Java.VScodeProject.Hello
Error: LinkageError occurred while loading main class Java.VScodeProject.Hello
java.lang.ClassFormatError: Incompatible magic value 3405691579 in class file Java/VScodeProject/Hello

其实 Java 创始人还想了一个魔数:CAFEHEAD,不过这个 CAFEHEAD 用到了 Java 的序列化文件的魔数了。

版本号 Minor Verson

版本号码就是紧跟着魔数的后面四个字节的内容,表示 Javac 编译时的版本号:

329.jpg

里面的 0x3B 就是 59 版本号,表示 Java 15 版本的 Javac 编译出来的。

1
2
3
4
5
6
7
8
Java 1.2 使用主要版本 46
Java 1.3 使用主要版本 47
Java 1.4 使用主要版本 48
Java 5 使用主要版本 49
...
Java 8 使用主要版本 52
...
Java 15 使用主要版本 59

当版本号大于 JVM 的版本的时候,加载就会报错 59 的 JVM 没法加载 74 Javac 编译的文件:

1
2
3
E:\Work>java Java.VScodeProject.Hello
Error: LinkageError occurred while loading main class Java.VScodeProject.Hello
java.lang.UnsupportedClassVersionError: Java/VScodeProject/Hello has been compiled by a more recent version of the Java Runtime (class file version 74.0), this version of the Java Runtime only recognizes class file versions up to 59.0

常量池

紧跟着后面的就是常量池了,主要存储复杂的常量(简单的如 0 这些直接内嵌到后面的代码中)。

这个常量池的结构是这样的,最前面的两个字节变量为 poolCount 表示一共有多少个常量,这里的 HelloWorld 是 0x001D,表示一共有 29 个,除掉保留的 id 0,一共有 28 个常量。然后这个 poolCount 后面的内容就是一块一块的常量表示了。一共有 28 块。

这 28 块的长度是不知道的,只能一个一个依次读取每一个常量块,然后确认里面的内容,直到读完 28 个之后,才结束对常量池的处理。

对于每一个常量块,是由 tag + 内容 组成的,下面是 tag 值和类型含义:

tag 常量项类型 含义
1 CONSTANT_Utf8 用于存储字符串的常量项。该项真正包含了字符串内容。而 CONSTANT_String 常量项只存储了一个指向 CONSTANT_Utf8 项的索引。
3 CONSTANT_Integer Java中,int 和 float 型数据的长度都是4个字节。这两种常量分别代表 int 和 float 型数据信息。
4 CONSTANT_Float 同上
5 CONSTANT_Long Java中, long 和 double 型数据的长度都是8个字节。这两种常量分别代表 long 以及 double 型数据信息。
6 CONSTANT_Double 同上
7 CONSTANT_Class 代表类或接口的信息。
8 CONSTANT_String 代表一个字符串(String)。该常量本身不存储字符串的内容,它只是存储了一个索引值。
9 CONSTANT_Fieldref 存储成员变量的信息。
10 CONSTANT_Methodref 存储成员函数的信息。
11 CONSTANT_InterfaceMethodref 存储接口函数的信息。
12 CONSTANT_NameAndType 这种类型的常量项用于描述类的成员域或成员函数相关的信息。
15 CONSTANT_MethodHandle 用于描述 MethodHandle 信息。MethodHandle 和反射有关系。Java类库中对应的类为 java.lang.invoke.MethodHandle 。
16 CONSTANT_MethodType 用于描述一个成员函数的信息。只包括函数的 参数类型 和 返回值 ,不包括函数名和所属类的类名。
18 CONSTANT_InvokeDynamic 用于 invokeDynamic 指令。invokeDynamic 和 Java 平台上实现了一些动态语言(如Python)相类似的有关功能。

3_Integer 4_Float

Java 中,int 和 float 型数据的长度都是 4 个字节。tag 3 和 tag 4 分别代表 int 和 float 型数据信息。

当你看到一块代码为: 03 00 00 00 01 表示 tag 为 Integer 整形的常量,值为 1。(需要说一下 JVM 使用的是大端模式来存储数据,所以这里是高位在前,和 x86 小端模式相反)

我们表示为:

1
2
3
4
CONSTANT_Integer_Info {
u1 tag; //使用一个字节装 tag
u4 bytes; // 使用四个字节装值
}

需要注意的是,Java 将 boolean char byte short int 这些小于等于四字节的非浮点数都以 3_Integer 来保存。

5_Long 6_Double

Java中,long 和 double 型数据的长度都是8个字节。这两种常量分别代表 long 以及 double 型数据信息。再 poolCount 中,Long 和 Double 是按照两个常量快的大小计算的(按照几个不是看实际占用容量,而是另外一套规则)。

1
2
3
4
5
6
7
8
9
CONSTANT_Long_Info {
u1 tag;
u8 bytes; //JVM 大端模式直接写就好
}

CONSTANT_Double_Info {
u1 tag;
u8 bytes;
}

1_UTF8

这是存储的是字符串真正内容的 tag ,而 8_String 存储的则是指向 1_UTF8 的一个索引。

1
2
3
4
5
CONSTANT_Utf8 {
u1 tag;
u2 length;
ulength bytes[length];
}

这里的 length 表示后面存储字符串真正内容的 bytes 长度,而不是字符串的长度。bytes[] 里面使用 MUTF-8 (Modified UTF-8)编码来存储字符串内容。

MUTF-8 和 UTF-8 只有两处不同:

  1. 对于 UTF-8 的一位的空字符 0x00,在 MUTF-8 使用 0xC080 两位表示,也就是:11000000 1000000(UTF-8 的两位 0x00 表示方法)

  2. 对于 UTF-8 的四字节字符,使用双字符表示。

剩下的单字符则,双字符,三字符和 UTF-8 一模一样。

8_String

前面说了,这个只是保存一个指向 1_UTF8 的索引(也就是常量块的 ID),从哪里找 String 的真正内容:

1
2
3
4
CONSTANT_String {
u1 tag;
u2 utf8StringIndex; //真正内容的存储地方
}

7_Class

用来表示类或者接口,同样不保存类的具体内容,而是指向一个 1_UTF8 的索引,里面保存了类的全限定名称,如 java/lang/System

表示方式:

1
2
3
4
CONSTANT_Class {
u1 tag;
u2 utf8ClassIndex;
}

12_NameAndType

使用 NameAndType 来表示字段和方法,记录了字段或者方法的名字(指向 1_utf8 的 ID)和描述符(同样是指向 1_utf8 的 ID),占用三个常量池 ID:

1
2
3
4
5
CONSTANT_NameAndType {
u1 tag;
u2 utf8NameIndex;
u2 utf8DescriptorIndex;
}

名字就是方法名或者字段名,而描述符的规则具体的可以看下面在介绍字段表和方法表里面的东西。

9_Fieldref 10_Methodref 11_InterfaceMethodref

三者主要都是用来标记在类中出现过的字段,方法的。

其中本类的所有字段都会以 Fieldref 的格式保存在常量池中。本类所创建的所有方法和代码调用的所有方法以 Methodref 格式保存,而代码调用过的接口的方法,则以 InterfaceMethodref 保存。

三者的结构模式都是一样的,固定的 tag 来区分常量块里面的内容,classIndex 指向一个 7_Class 的 ID 描述此字段方法接口所属的 Class 的信息,nameAndTypeIndex 指向一个 12_NameAndType 的 ID 描述

1
2
3
4
5
CONSTANT_Field/Method/InterfaceMethod-ref {
u1 tag;
u2 classIndex;
u2 nameAndTypeIndex;
}

15_MethodHandle 16_MethodType 18_InvokeDynamic

这些主要是为了更好的支持动态语言调用出现的。具体内容就不多介绍了。

访问标记

紧接着常量池的就是访问标记了,集合成两个字节。

330.jpg

This_Class Super_name Interfaces

这三个都是用来记录当前类索引,父类索引,接口索引的。

每个占据两个二字节,分别指向一个 7_Class 和 1_Utf8 类型,记录类或接口信息,和名字。

字段表

基础结构

是一个类似于常量池的变长结构,是用来记录整个类中的所有字段的信息的。最大的结构:

1
2
3
4
FieldTable {
u2 fieldsCount //类种的字段数量
FieldInfo[] Fields[fieldsCount] //多个 FieldInfo 组成的每个字段的信息
}

然后就是描述字段信息的 FieldInfo 了:

1
2
3
4
5
6
7
FieldInfo {
u2 accessFlags //两个字节,字段访问标记,原理和上面的类访问标记一样
u2 nameIndex //1_utf8 的常量池地址,字段名
u2 descriptorIndex //1_utf8 常量池地址,字段描述符
u2 attributesCount //字段属性数量
attributeInfo attributtes[attributesCount] //字段属性具体内容。
}

后面主要介绍一下字段描述符和字段属性具体内容:

字段描述符规则

描述符 类型
B byte
C char
D double
F floats
I int
J long
S short
Z boolean
‘L’+’FullClassName’+’;’ 引用类型
[ 一个维度的数组

举个例子,String[] 被描述为:[Ljava/lang/String;[ 为一维数组,L 表示为引用类型,java/lang/String 为引用类型的全类名,; 为结束符号。

字段属性具体内容

先欠着,以后再写。

方法表

方法表的结构和 字段表差不多:

1
2
3
4
MethodTable {
u2 methodsCount
MethodInfo[] methods[MethodCount]
}
1
2
3
4
5
6
7
MethodInfo {
u2 accessFlags
u2 nameIndex
u2 descriptorIndex
u2 attributesCount
AttributeInfo attributes[attributesCount]
}

方法名和描述符

对于方法名索引 nameIndex 就是一个指向 utf8Info 的常量,就是方法的名字

描述符的格式则是:(A)B 其中 A 是方法的参数,里面参数的描述符和字段的格式是一样的,挨在一起,本身位置就在参数里面的位置,B 表示方法的返回值。

(IDLjava/lang/String;)Ljava/lang/Object; 所表示的方法就是:

Object method(int param1,double param2,String param3);