从零理解java之class文件内容解读

听多了谎话,看够了繁华,别让一句我爱你,只经过口腔,不经过心脏

Posted by yishuifengxiao on 2023-10-17

原始代码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
package org.example;

public class Demo {

public static void main(String[] args) {

int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
}
}

一 class文件读取

1.1 使用十六进制编辑器打开

内容如下:

class内容

1.2 使用字节方法读取(10进制)

结果为:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\desktop\\Demo.class"));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = bis.read(bytes)) != -1) {
List<String> list = new ArrayList<>();
for (int i = 0; i < len; i++) {
list.add(" " + (bytes[i]));
}
list.stream().forEach(System.out::print);
}
}

运行结果为

1
-54 -2 -70 -66 0 0 0 55 0 37 10 0 5 0 24 9 0 25 0 26 10 0 27 0 28 7 0 29 7 0 30 1 0 6 60 105 110 105 116 62 1 0 3 40 41 86 1 0 4 67 111 100 101 1 0 15 76 105 110 101 78 117 109 98 101 114 84 97 98 108 101 1 0 18 76 111 99 97 108 86 97 114 105 97 98 108 101 84 97 98 108 101 1 0 4 116 104 105 115 1 0 18 76 111 114 103 47 101 120 97 109 112 108 101 47 68 101 109 111 59 1 0 4 109 97 105 110 1 0 22 40 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 41 86 1 0 4 97 114 103 115 1 0 19 91 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 1 0 1 97 1 0 1 73 1 0 1 98 1 0 1 99 1 0 16 77 101 116 104 111 100 80 97 114 97 109 101 116 101 114 115 1 0 10 83 111 117 114 99 101 70 105 108 101 1 0 9 68 101 109 111 46 106 97 118 97 12 0 6 0 7 7 0 31 12 0 32 0 33 7 0 34 12 0 35 0 36 1 0 16 111 114 103 47 101 120 97 109 112 108 101 47 68 101 109 111 1 0 16 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 116 1 0 16 106 97 118 97 47 108 97 110 103 47 83 121 115 116 101 109 1 0 3 111 117 116 1 0 21 76 106 97 118 97 47 105 111 47 80 114 105 110 116 83 116 114 101 97 109 59 1 0 19 106 97 118 97 47 105 111 47 80 114 105 110 116 83 116 114 101 97 109 1 0 7 112 114 105 110 116 108 110 1 0 4 40 73 41 86 0 33 0 4 0 5 0 0 0 0 0 2 0 1 0 6 0 7 0 1 0 8 0 0 0 47 0 1 0 1 0 0 0 5 42 -73 0 1 -79 0 0 0 2 0 9 0 0 0 6 0 1 0 0 0 14 0 10 0 0 0 12 0 1 0 0 0 5 0 11 0 12 0 0 0 9 0 13 0 14 0 2 0 8 0 0 0 104 0 2 0 4 0 0 0 16 4 60 5 61 27 28 96 62 -78 0 2 29 -74 0 3 -79 0 0 0 2 0 9 0 0 0 22 0 5 0 0 0 18 0 2 0 19 0 4 0 20 0 8 0 21 0 15 0 22 0 10 0 0 0 42 0 4 0 0 0 16 0 15 0 16 0 0 0 2 0 14 0 17 0 18 0 1 0 4 0 12 0 19 0 18 0 2 0 8 0 8 0 20 0 18 0 3 0 21 0 0 0 5 1 0 15 0 0 0 1 0 22 0 0 0 2 0 23

1.3 使用字符读取

1
2
3
4
5
6
7
private static void readLine() throws IOException {
BufferedReader bw = new BufferedReader(new FileReader(new File("D:\\desktop\\Demo.class")));
String line = null;
while ((line = bw.readLine()) != null) {
System.out.println(line);
}
}

结果为

1
2
3
4
5
6
7
����   7 %
   
    <init> ()V Code LineNumberTable LocalVariableTable this Lorg/example/Demo; main ([Ljava/lang/String;)V args [Ljava/lang/String; a I b c MethodParameters
SourceFile Demo.java    ! " # $ org/example/Demo java/lang/Object java/lang/System out Ljava/io/PrintStream; java/io/PrintStream println (I)V !        /   *� �    
 
  h   <=`>� � �           
*                       

1.4 十六进制读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void readByte() throws IOException {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\desktop\\Demo.class"));
byte[] bytes = new byte[8];
int len = 0;
while ((len = bis.read(bytes)) != -1) {
List<String> list = new ArrayList<>();
for (int i = 0; i < len; i++) {
byte bte = bytes[i];
// 原始值+16机制显示
String text = " " + bte + "(" + (Integer.toHexString(bte & 0xFF)) + ")";
System.out.print(text);
//十六进制打印
System.out.printf("%02X ", bte);
}
System.out.println();

}
}

得到的结果为

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
67
68
69
70
71
72
73
74
75
-54(ca)CA  -2(fe)FE  -70(ba)BA  -66(be)BE  0(0)00  0(0)00  0(0)00  55(37)37 
0(0)00 37(25)25 10(a)0A 0(0)00 5(5)05 0(0)00 24(18)18 9(9)09
0(0)00 25(19)19 0(0)00 26(1a)1A 10(a)0A 0(0)00 27(1b)1B 0(0)00
28(1c)1C 7(7)07 0(0)00 29(1d)1D 7(7)07 0(0)00 30(1e)1E 1(1)01
0(0)00 6(6)06 60(3c)3C 105(69)69 110(6e)6E 105(69)69 116(74)74 62(3e)3E
1(1)01 0(0)00 3(3)03 40(28)28 41(29)29 86(56)56 1(1)01 0(0)00
4(4)04 67(43)43 111(6f)6F 100(64)64 101(65)65 1(1)01 0(0)00 15(f)0F
76(4c)4C 105(69)69 110(6e)6E 101(65)65 78(4e)4E 117(75)75 109(6d)6D 98(62)62
101(65)65 114(72)72 84(54)54 97(61)61 98(62)62 108(6c)6C 101(65)65 1(1)01
0(0)00 18(12)12 76(4c)4C 111(6f)6F 99(63)63 97(61)61 108(6c)6C 86(56)56
97(61)61 114(72)72 105(69)69 97(61)61 98(62)62 108(6c)6C 101(65)65 84(54)54
97(61)61 98(62)62 108(6c)6C 101(65)65 1(1)01 0(0)00 4(4)04 116(74)74
104(68)68 105(69)69 115(73)73 1(1)01 0(0)00 18(12)12 76(4c)4C 111(6f)6F
114(72)72 103(67)67 47(2f)2F 101(65)65 120(78)78 97(61)61 109(6d)6D 112(70)70
108(6c)6C 101(65)65 47(2f)2F 68(44)44 101(65)65 109(6d)6D 111(6f)6F 59(3b)3B
1(1)01 0(0)00 4(4)04 109(6d)6D 97(61)61 105(69)69 110(6e)6E 1(1)01
0(0)00 22(16)16 40(28)28 91(5b)5B 76(4c)4C 106(6a)6A 97(61)61 118(76)76
97(61)61 47(2f)2F 108(6c)6C 97(61)61 110(6e)6E 103(67)67 47(2f)2F 83(53)53
116(74)74 114(72)72 105(69)69 110(6e)6E 103(67)67 59(3b)3B 41(29)29 86(56)56
1(1)01 0(0)00 4(4)04 97(61)61 114(72)72 103(67)67 115(73)73 1(1)01
0(0)00 19(13)13 91(5b)5B 76(4c)4C 106(6a)6A 97(61)61 118(76)76 97(61)61
47(2f)2F 108(6c)6C 97(61)61 110(6e)6E 103(67)67 47(2f)2F 83(53)53 116(74)74
114(72)72 105(69)69 110(6e)6E 103(67)67 59(3b)3B 1(1)01 0(0)00 1(1)01
97(61)61 1(1)01 0(0)00 1(1)01 73(49)49 1(1)01 0(0)00 1(1)01
98(62)62 1(1)01 0(0)00 1(1)01 99(63)63 1(1)01 0(0)00 16(10)10
77(4d)4D 101(65)65 116(74)74 104(68)68 111(6f)6F 100(64)64 80(50)50 97(61)61
114(72)72 97(61)61 109(6d)6D 101(65)65 116(74)74 101(65)65 114(72)72 115(73)73
1(1)01 0(0)00 10(a)0A 83(53)53 111(6f)6F 117(75)75 114(72)72 99(63)63
101(65)65 70(46)46 105(69)69 108(6c)6C 101(65)65 1(1)01 0(0)00 9(9)09
68(44)44 101(65)65 109(6d)6D 111(6f)6F 46(2e)2E 106(6a)6A 97(61)61 118(76)76
97(61)61 12(c)0C 0(0)00 6(6)06 0(0)00 7(7)07 7(7)07 0(0)00
31(1f)1F 12(c)0C 0(0)00 32(20)20 0(0)00 33(21)21 7(7)07 0(0)00
34(22)22 12(c)0C 0(0)00 35(23)23 0(0)00 36(24)24 1(1)01 0(0)00
16(10)10 111(6f)6F 114(72)72 103(67)67 47(2f)2F 101(65)65 120(78)78 97(61)61
109(6d)6D 112(70)70 108(6c)6C 101(65)65 47(2f)2F 68(44)44 101(65)65 109(6d)6D
111(6f)6F 1(1)01 0(0)00 16(10)10 106(6a)6A 97(61)61 118(76)76 97(61)61
47(2f)2F 108(6c)6C 97(61)61 110(6e)6E 103(67)67 47(2f)2F 79(4f)4F 98(62)62
106(6a)6A 101(65)65 99(63)63 116(74)74 1(1)01 0(0)00 16(10)10 106(6a)6A
97(61)61 118(76)76 97(61)61 47(2f)2F 108(6c)6C 97(61)61 110(6e)6E 103(67)67
47(2f)2F 83(53)53 121(79)79 115(73)73 116(74)74 101(65)65 109(6d)6D 1(1)01
0(0)00 3(3)03 111(6f)6F 117(75)75 116(74)74 1(1)01 0(0)00 21(15)15
76(4c)4C 106(6a)6A 97(61)61 118(76)76 97(61)61 47(2f)2F 105(69)69 111(6f)6F
47(2f)2F 80(50)50 114(72)72 105(69)69 110(6e)6E 116(74)74 83(53)53 116(74)74
114(72)72 101(65)65 97(61)61 109(6d)6D 59(3b)3B 1(1)01 0(0)00 19(13)13
106(6a)6A 97(61)61 118(76)76 97(61)61 47(2f)2F 105(69)69 111(6f)6F 47(2f)2F
80(50)50 114(72)72 105(69)69 110(6e)6E 116(74)74 83(53)53 116(74)74 114(72)72
101(65)65 97(61)61 109(6d)6D 1(1)01 0(0)00 7(7)07 112(70)70 114(72)72
105(69)69 110(6e)6E 116(74)74 108(6c)6C 110(6e)6E 1(1)01 0(0)00 4(4)04
40(28)28 73(49)49 41(29)29 86(56)56 0(0)00 33(21)21 0(0)00 4(4)04
0(0)00 5(5)05 0(0)00 0(0)00 0(0)00 0(0)00 0(0)00 2(2)02
0(0)00 1(1)01 0(0)00 6(6)06 0(0)00 7(7)07 0(0)00 1(1)01
0(0)00 8(8)08 0(0)00 0(0)00 0(0)00 47(2f)2F 0(0)00 1(1)01
0(0)00 1(1)01 0(0)00 0(0)00 0(0)00 5(5)05 42(2a)2A -73(b7)B7
0(0)00 1(1)01 -79(b1)B1 0(0)00 0(0)00 0(0)00 2(2)02 0(0)00
9(9)09 0(0)00 0(0)00 0(0)00 6(6)06 0(0)00 1(1)01 0(0)00
0(0)00 0(0)00 14(e)0E 0(0)00 10(a)0A 0(0)00 0(0)00 0(0)00
12(c)0C 0(0)00 1(1)01 0(0)00 0(0)00 0(0)00 5(5)05 0(0)00
11(b)0B 0(0)00 12(c)0C 0(0)00 0(0)00 0(0)00 9(9)09 0(0)00
13(d)0D 0(0)00 14(e)0E 0(0)00 2(2)02 0(0)00 8(8)08 0(0)00
0(0)00 0(0)00 104(68)68 0(0)00 2(2)02 0(0)00 4(4)04 0(0)00
0(0)00 0(0)00 16(10)10 4(4)04 60(3c)3C 5(5)05 61(3d)3D 27(1b)1B
28(1c)1C 96(60)60 62(3e)3E -78(b2)B2 0(0)00 2(2)02 29(1d)1D -74(b6)B6
0(0)00 3(3)03 -79(b1)B1 0(0)00 0(0)00 0(0)00 2(2)02 0(0)00
9(9)09 0(0)00 0(0)00 0(0)00 22(16)16 0(0)00 5(5)05 0(0)00
0(0)00 0(0)00 18(12)12 0(0)00 2(2)02 0(0)00 19(13)13 0(0)00
4(4)04 0(0)00 20(14)14 0(0)00 8(8)08 0(0)00 21(15)15 0(0)00
15(f)0F 0(0)00 22(16)16 0(0)00 10(a)0A 0(0)00 0(0)00 0(0)00
42(2a)2A 0(0)00 4(4)04 0(0)00 0(0)00 0(0)00 16(10)10 0(0)00
15(f)0F 0(0)00 16(10)10 0(0)00 0(0)00 0(0)00 2(2)02 0(0)00
14(e)0E 0(0)00 17(11)11 0(0)00 18(12)12 0(0)00 1(1)01 0(0)00
4(4)04 0(0)00 12(c)0C 0(0)00 19(13)13 0(0)00 18(12)12 0(0)00
2(2)02 0(0)00 8(8)08 0(0)00 8(8)08 0(0)00 20(14)14 0(0)00
18(12)12 0(0)00 3(3)03 0(0)00 21(15)15 0(0)00 0(0)00 0(0)00
5(5)05 1(1)01 0(0)00 15(f)0F 0(0)00 0(0)00 0(0)00 1(1)01
0(0)00 22(16)16 0(0)00 0(0)00 0(0)00 2(2)02 0(0)00 23(17)17

二 Class类文件的结构

Class文件结构的内容,绝大部分都是在第一版的《Java虚拟机规范》(1997年发布,对应于JDK 1.2时代的Java虚拟机)中就已经定义好的,内容虽然古老,但时至今日,Java发展经历了十余个大版本、无数小更新,那时定义的Class文件格式的各项细节几乎没有出现任何改变。尽管不同版本的《Java虚拟机规范》对Class文件格式进行了几次更新,但基本上只是在原有结构基础上新增内容、扩充功能,并未对已定义的内容做出修改。

注意 任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)。本文通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它完全不需要以磁盘文件的形式存在

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。

根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基础,所以这里笔者必须先解释清楚这两个概念。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表,这张表由下表所示的数据项按严格顺序排列构成。

image-20231017095718891

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。

Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在表6-1中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变

2.1 魔数与版本号

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件

1
-54(ca)CA  -2(fe)FE  -70(ba)BA  -66(be)BE  0(0)00  0(0)00  0(0)00  55(37)37

可以清楚地看见开头4个字节的十六进制表示是0xCAFEBABE,代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x0037,也即是十进制的55,该版本号说明这个是可以被JDK 11或以上版本虚拟机执行的Class文件。

image-20231017101354203

从JDK 9开始,Javac编译器不再支持使用-source参数编译版本号小于1.5的源码。

关于次版本号,曾经在现代Java(即Java 2)出现前被短暂使用过,JDK 1.0.2支持的版本45.0~45.3(包括45.0~45.3)。JDK 1.1支持版本45.0~45.65535,从JDK 1.2以后,直到JDK 12之前次版本号均未使用,全部固定为零。而到了JDK 12时期,由于JDK提供的功能集已经非常庞大,有一些复杂的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功能特性的支持。如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则必须把次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分出来

2.2 常量池

紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,如结果所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0025,即十进制的37,这就代表常量池中有36项常量,索引值范围为1~36。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始

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
public class ByteParser {

public static void main(String[] args) throws IOException {
List<Byte> list = readByte();
//在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count) 即9-10位
final List<Byte> bytes = list.subList(8, 10);
final String val = bytes.stream().map(ByteParser::toHexString).collect(Collectors.joining());
System.out.println("0x" + val); // 输出 0x0025
System.out.println(new BigInteger(val, 16).intValue()); // 输出 37
}

private static String toHexString(Byte bte) {
final String hexString = Integer.toHexString(bte & 0xFF);
return hexString.length() == 2 ? hexString : "0" + hexString;
}

private static List<Byte> readByte() throws IOException {
List<Byte> list = new ArrayList<>();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\desktop\\Demo.class"));
byte[] bytes = new byte[1024];
int len = 0;
while ((len = bis.read(bytes)) != -1) {

for (int i = 0; i < len; i++) {
byte bte = bytes[i];
list.add(bte);
}
}
return list;
}
}

读取到的常量内容区为:

1
2
3
4
5
10(a)0A  0(0)00  5(5)05  0(0)00  24(18)18  9(9)09 
0(0)00 25(19)19 0(0)00 26(1a)1A 10(a)0A 0(0)00 27(1b)1B 0(0)00
28(1c)1C 7(7)07 0(0)00 29(1d)1D 7(7)07 0(0)00 30(1e)1E 1(1)01
0(0)00 6(6)06 60(3c)3C 105(69)69 110(6e)6E 105(69)69 116(74)74 62(3e)3E
1(1)01 0(0)00 3(3)03 40(28)28 41(29)29 86(56)56 1(1)01

image-20231017104316121

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统(Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位(tag,取值见表中标志列),代表着当前常量属于哪种常量类型

image-20231017105357512

之所以说常量池是最烦琐的数据,是因为这17种常量类型各自有着完全独立的数据结构,两两之间并没有什么共性和联系,因此只能逐项进行讲解

回头看常量池的第一项常量,它的标志位(偏移地址:0x0000000A)是0x0A,查表的标志列可知这个常量属于CONSTANT_Methodref_info类型,此类型的常量代表类中方法的符号引用。CONSTANT_Methodref_info的结构如下

image-20231017110411103

为了快速分析,可以借助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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
D:\desktop
λ javap --help
用法: javap <options> <classes>
其中, 可能的选项包括:
-? -h --help -help 输出此帮助消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
--module <模块>, -m <模块> 指定包含要反汇编的类的模块
--module-path <路径> 指定查找应用程序模块的位置
--system <jdk> 指定查找系统模块的位置
--class-path <路径> 指定查找用户类文件的位置
-classpath <路径> 指定查找用户类文件的位置
-cp <路径> 指定查找用户类文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置

GNU 样式的选项可使用 = (而非空白) 来分隔选项名称
及其值。

每个类可由其文件名, URL 或其
全限定类名指定。示例:
path/to/MyClass.class
jar:file:///path/to/MyJar.jar!/mypkg/MyClass.class
java.lang.Object


D:\desktop
λ javap -c -v -s demo.class
Classfile /D:/desktop/demo.class
Last modified 2023年10月16日; size 600 bytes
MD5 checksum 5a4ee73b0279170fc31292721ecc8432
Compiled from "Demo.java"
public class org.example.Demo
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // org/example/Demo
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #27.#28 // java/io/PrintStream.println:(I)V
#4 = Class #29 // org/example/Demo
#5 = Class #30 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lorg/example/Demo;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 I
#19 = Utf8 b
#20 = Utf8 c
#21 = Utf8 MethodParameters
#22 = Utf8 SourceFile
#23 = Utf8 Demo.java
#24 = NameAndType #6:#7 // "<init>":()V
#25 = Class #31 // java/lang/System
#26 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#27 = Class #34 // java/io/PrintStream
#28 = NameAndType #35:#36 // println:(I)V
#29 = Utf8 org/example/Demo
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (I)V
{
public org.example.Demo();
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 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/Demo;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: return
LineNumberTable:
line 18: 0
line 19: 2
line 20: 4
line 21: 8
line 22: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
2 14 1 a I
4 12 2 b I
8 8 3 c I
MethodParameters:
Name Flags
args
}
SourceFile: "Demo.java"

从上面的结果可以看出,计算机已经帮我们把整个常量池的21项常量都计算了出来,并且计算结果与我们手工计算的结果完全一致。仔细看一下会发现,其中有些常量似乎从来没有在代码中出现过如IV<init>LineNumberTableLocalVariableTable等,这些看起来在源代码中不存在的常量是哪里来的?

这部分常量的确不来源于Java源代码,它们都是编译器自动生成的,会被字段表(field_info)、方法表(method_info)、属性表(attribute_info)所引用,它们将会被用来描述一些不方便使用“固定字节”进行表达的内容,譬如描述方法的返回值是什么,有几个参数,每个参数的类型是什么。因为Java中的“类”是无穷无尽的,无法通过简单的无符号数来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达

image-20231017111235258

image-20231017111246489

image-20231017111256344访问标志

2.3 访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;

image-20231017111539915

access_flags中一共有16个标志位可以使用,当前只定义了其中9个[1],没有使用到的标志位要求一律为零。以上面的代码为例,Demo是一个普通Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。从图6-5中看到,access_flags标志(偏移地址:0x00000184)的确为0x0021。

image-20231017130633911

为了验证此功能,将代码修改为

image-20231017130752101

此时理论上该值为::0x0001|0x0020|0x0010=0x0031

查看重新的生成的class文件结构:

image-20231017130930646

与猜测值把持一致,证明设想正确。

三 字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构(这两种架构的执行过程、区别和影响将在第8章中探讨),所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

字节码指令集可算是一种具有鲜明特点、优势和劣势均很突出的指令集架构,由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不能够超过256条。

在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。举个例子,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立的操作码。

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确指明操作类型的字母,例如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,例如无条件跳转指令goto则是与数据类型无关的指令。

因为Java虚拟机的操作码长度只有一字节,所以包含了数据类型的操作码就为指令集的设计带来了很大的压力:如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那么指令的数量恐怕就会超出一字节所能表示的数量范围了。因此,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它,换句话说,指令集将会被故意设计成非完全独立的。(《Java虚拟机规范》中把这种特性称为“Not Orthogonal”,即并非每种数据类型和每一种操作都有对应的指令。)有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。下表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到一个具体的字节码指令。如果在表中指令模板与数据类型两列共同确定的格为空,则说明虚拟机不支持对这种数据类型执行这项操作。例如load指令有操作int类型的iload,但是没有操作byte类型的同类指令

image-20231017171526536