JVM学习总结之『一个类的前世今生』

1 我乃中山靖王之后

大家好,我叫李大锤,是一名不入流的演员,我即将参演一部名叫《三国演义》的舞台剧,导演是棺材板按不住的罗贯中老先生。而我,即将扮演三位主角之一的刘皇叔,嘿嘿,想想还有点小激动呢!

按照剧本,我是一名出生低微的屌丝,被嘲笑为“织席贩履”之辈,所以一开始,我长这个B样:

1
2
public class LiuBei {
}

然而,我开局就会收关张两位挂逼做小弟,于是,我变成了:

1
2
3
4
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
}

算了,就不追求麾下武将如云谋士如雨了,人太多写的也累,有二弟和三弟出场就够了。

当然,作为未来的汉昭烈帝,我开局还会一些特殊技能,不亏是主角之一,这技能真是别具一格:

1
2
3
4
5
6
7
8
9
10
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}

好了,身残志坚,躺在棺材里还在coding的著名程序员罗贯中先生,已经通过他精湛的代码功底,为我编写了一个详(jian)细(lou)的开局设定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}
}

现在,让我们去为舞台剧做一下准备吧!

2 新手村

舞台剧开演在即,来,摄影机往前,我们先来俯瞰一下整个会场的布局吧(详细介绍见:JAVA内存结构和内存管理):

首先,面积最为广大的,就是我们舞台的后台,我们唤作,所有有名有幸的三国豪杰们(对象们),都会在后台齐聚,各自准备。

然后,我们可以看到一块巨大的显示屏,我们唤作方法区,上面是本剧的台本,上面写着:

  • 各个英雄豪杰的设定/经历(类信息)等信息
    • 刘备会遇到关张,然后还会摔儿子技能(属性,方法)
    • 曹操麾下有曹仁曹纯夏侯兄弟等挂逼,还有好人妻这个技能。(属性,方法)
    • ….
  • 一些人尽皆知的信息(常量)
    • 比如现在是东汉末年,嗯,比如东汉末年是个常量。
  • 某位英雄广为人知的设定(类的静态变量)。
    • 刘备:说织席贩履的给老子滚出来啊魂淡!!
    • 曹操:梦中杀伦什么的,我真不是故意的。
    • 孙权:就不能不提合肥,不提孙十万吗。。

舞台之上,我们看到了有三束聚光灯各自照亮舞台一隅,这是以我们三位主角曹孙刘为视角的三个线程,然后被聚光灯照亮的三块方寸之地,主要是虚拟机栈本地方法栈,还有一个小的牌子,叫做程序计数器,用来标记此间主角演到剧情的何处了。

3 英雄要问出身

逛完了舞台以后,我得去看看我的台本,虽然我在接戏之前已经知道了罗贯中老先生为我量身定做的草稿:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LiuBei {
int zhangFei=123;//张飞
Object guanYu=new Object();//关羽 为了嫌麻烦,就不给他们定制特定的类了,就int & Object
public void shuaiErZi() {//摔儿子
}

public void shouMaiRenXin() {//收买人心
shuaiErZi();
}
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}
}

但草稿只是草稿,正经的舞台剧,肯定不能用这么简陋的东西来演出,不说别的,看草稿我只知道我有关张两个小弟,但演出时,我至少得知道关张是谁来演,我到底和谁撘对手戏吧?是胡歌还是霍建华?

所以,还需要把草稿再加工,变成真正的台本,这个过程,叫做编译,这时,java文件会编译成class文件。

class文件的内容我们不再赘述,详情在JAVA Class文件和类加载机制一文中可见。我们只要记得几个核心要素:

  • 类型信息包含魔数,主次版本号等。

  • 常量池里面存放着字面量和符号引用。

    • 常量池中每一项常量都是一个表,在JDK1.7之后共有14种表结构,这14种常量类型各自有自己的结构,下面列出每个常量项的结构及含义
    • 字面量可以理解为就是字符文本,class文件中的其他信息要用到字符文本的时候,都是“引用”他,比如字段表中,刘备有关羽这个小弟,那“关羽”这个名字的文本就存放于常量池中。
    • 符号引用包含下面三类:
      • 全限定名:就是类名全称,例如:org/xxx/class/testClass
      • 简单名称:即没有类型和参数修饰的字段或者方法名称,例如方法test()的简单名称就是test,m字段的简单名称就是m。
      • 描述符:描述符的作用是描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值。根据描述符的规则,基本数据类型以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名表示
        • 如“viod main(String[] args)” 的描述符为“([Ljava/lang/String;)V
        • 如“String[][]”,会被记录为”[[Ljava/lang/String”
        • “int[]”被记录为“[I”。
  • 字段表集合,记录这个类的字段信息,比如我们刘备拥有关张两个小弟做对象,

    • 这里我们会记录关张的字段名称,如关羽的名称的值就是常量池中“关羽”常量的引用
    • 记录描述符(descriptor_index中记录),描述这个字段的类型,我们这里是Object类型,那么这个值就是指向常量池中的“Ljava/lang/Object”常量的引用。
    • 以及各种修饰符:类似于:汉寿亭侯·美髯公·武圣·刮骨疗法临床实验者·季汉扛把子·关羽=public transient volatile Object guanYu。
  • 方法表集合,记录这个类的方法信息,比如我们刘备拥有摔儿子和收买人心方法。

    • 这里我们会记录方法的名称,同样引用常量池。
    • 记录描述符(descriptor_index中记录),描述这个方法的描述符,我们这里是void shuaiErZi(),那么这个值就是指向常量池中的“()V”常量的引用。(注意这里的V是指void,描述符不包括方法名称)
    • 以及各种修饰符:类似于:作用全场的·效果拔群的·刘备角色固有的·摔儿子=public volatile static shuaiErZi
    • 方法体里面有代码的,都会有一个code属性(引用属性表集合),里面有摔儿子说明文本长度(属性长度),操作数栈最大深度等,还有摔儿子的具体操作步骤(代码的字节码指令)。

来,我们使用javap工具

javap -c -v -p -l -constants /home/lisheng/IdeaProjects/learning/out/production/learning/com/company/LiuBei.class

将public class LiuBei的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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
Classfile /home/lisheng/IdeaProjects/learning/out/production/learning/com/company/LiuBei.class
Last modified 2019-12-10; size 1018 bytes
MD5 checksum 7133ac0c7e83a62e1081db1945bc6cf9
Compiled from "LiuBei.java"
public class com.company.LiuBei
minor version: 0 //次版本
major version: 52 //主版本
flags: ACC_PUBLIC, ACC_SUPER //LiuBei类的修饰符
Constant pool: //类的常量池
#1 = Methodref #3.#32 // java/lang/Object."<init>":()V
//#1表示常量池index,此处是一个Methodref类型的常量。
//《JAVA Class文件和类加载机制》一文我们知道Methodref类型内有两个index字段,又引用了两个常量。分别表示方法所属类全限定名,以及方法描述符。
//所以#3.#32 即为#1 = Methodref引用了index=3和index=32的常量。
//我们知道#3是Class类型,引用了#34= java/lang/Object,所以其实#3就是 java/lang/Object的类常量。这表示#1代表的方法是 java/lang/Object类的方法。
//我们知道#32是NameAndType类型常量,又引用了 #20=<init>,#21=()V,合起来就是#32存储了#1代表的方法的方法描述符。
//如此,我们得到了一个完整的Methodref,其内容记录了方法所在类的全限定名以及方法描述符。
//下面以此类推,不再赘述

#2 = Fieldref #15.#33 // com/company/LiuBei.zhangFei:I
#3 = Class #34 // java/lang/Object
#4 = Fieldref #15.#35 // com/company/LiuBei.guanYu:Ljava/lang/Object;
#5 = Methodref #15.#36 // com/company/LiuBei.shuaiErZi:()V
#6 = Fieldref #37.#38 // java/lang/System.out:Ljava/io/PrintStream;
#7 = Class #39 // java/lang/StringBuilder
#8 = Methodref #7.#32 // java/lang/StringBuilder."<init>":()V
#9 = Methodref #7.#40 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#10 = String #41 //
#11 = Methodref #7.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#12 = Methodref #7.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #44.#45 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#14 = Methodref #3.#43 // java/lang/Object.toString:()Ljava/lang/String;
#15 = Class #46 // com/company/LiuBei
#16 = Utf8 zhangFei
#17 = Utf8 I
#18 = Utf8 guanYu
#19 = Utf8 Ljava/lang/Object;
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 Lcom/company/LiuBei;
#27 = Utf8 shuaiErZi
#28 = Utf8 shouMaiRenXin
#29 = Utf8 geiWoShang
#30 = Utf8 SourceFile
#31 = Utf8 LiuBei.java
#32 = NameAndType #20:#21 // "<init>":()V
#33 = NameAndType #16:#17 // zhangFei:I
#34 = Utf8 java/lang/Object
#35 = NameAndType #18:#19 // guanYu:Ljava/lang/Object;
#36 = NameAndType #27:#21 // shuaiErZi:()V
#37 = Class #47 // java/lang/System
#38 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#39 = Utf8 java/lang/StringBuilder
#40 = NameAndType #50:#51 // append:(I)Ljava/lang/StringBuilder;
#41 = Utf8
#42 = NameAndType #50:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = NameAndType #53:#54 // toString:()Ljava/lang/String;
#44 = Class #55 // java/io/PrintStream
#45 = NameAndType #56:#57 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#46 = Utf8 com/company/LiuBei
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 append
#51 = Utf8 (I)Ljava/lang/StringBuilder;
#52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#53 = Utf8 toString
#54 = Utf8 ()Ljava/lang/String;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 printf
#57 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
{
int zhangFei;//字段表,张飞这个字段,name_index指向的是常量池的#16=zhangFei
descriptor: I//descriptor_index指向的是常量池的#17=I,表示类型是int
flags:

java.lang.Object guanYu;//字段表,关羽这个字段,name_index指向的是常量池的#18=guanYu
descriptor: Ljava/lang/Object;//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示类型是object类
flags:

public com.company.LiuBei();//这里开始是方法表,LiuBei()是LiuBei类的默认构造器。name_index指向常量池LiuBei字面量。
descriptor: ()V//descriptor_index指向常量池的#21 = Utf8 ()V
flags: ACC_PUBLIC
Code://code属性,存储着构造器的字节码指令
stack=3, locals=1, args_size=1//
0: aload_0
//从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈
1: invokespecial #1 // Method java/lang/Object."<init>":()V
//出栈,invokespecial表示调用方法,调用哪个方法呢,调用#1代表的java/lang/Object."<init>":()V 方法初始化对象,就是this指定的对象的init()方法完成初始化
4: aload_0
//再一次从本地变量表中加载索引为0的变量的值,也即this的引用,压入栈
5: bipush 123
//将123常量压入栈,当int取值-128~127时,JVM采用bipush指令将常量压入操作数栈中。
7: putfield #2 // Field zhangFei:I
// 将123赋值给zhangFei
//下面同理,new一个Object对象,再执行Object的<init>方法,然后赋值给guanyu,返回。
10: aload_0
11: new #3 // class java/lang/Object
14: dup
15: invokespecial #1 // Method java/lang/Object."<init>":()V
18: putfield #4 // Field guanYu:Ljava/lang/Object;
21: return
LineNumberTable:
//指令与代码行数的偏移对应关系,每一行第一个数字对应代码行数,第二个数字对应前面code中指令前面的数字
line 3: 0
line 4: 4
line 5: 10
LocalVariableTable:
//局部变量表,start+length表示这个变量在字节码中的生命周期起始和结束的偏移位置
//slot就是这个变量在局部变量表中的槽位(槽位可复用),name就是变量名称,Signatur局部变量类型描述

Start Length Slot Name Signature
0 22 0 this Lcom/company/LiuBei;
//下面同理,不再赘述
public void shuaiErZi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/company/LiuBei;

public void shouMaiRenXin();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #5 // Method shuaiErZi:()V
4: return
LineNumberTable:
line 10: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/company/LiuBei;

public void geiWoShang();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
10: aload_0
11: getfield #2 // Field zhangFei:I
14: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: ldc #10 // String
19: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: iconst_0
26: anewarray #3 // class java/lang/Object
29: invokevirtual #13 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
32: pop
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
41: return
LineNumberTable:
line 13: 0
line 14: 33
line 15: 41
LocalVariableTable:
Start Length Slot Name Signature
0 42 0 this Lcom/company/LiuBei;
}
SourceFile: "LiuBei.java"

看完了上面的反解析内容,我们要明白:方法表和常量池里面Methodref的区别:

  • 前者包含包括代码在内的全部方法信息,而后者充其量翻译出来,只包含了方法名+方法描述符+所在类限定名。
  • Methodref顾名思义,只是一个引用,是作为字节码的参数而存在的,如invokespecial #1,#1就是一个Methodref。
    • 所以我们可以看到常量池中存在Methodref=com/company/LiuBei.shuaiErZi:()V,却不存在Methodref=com/company/LiuBei.shouMaiRenXin:()V,因为shouMaiRenXin方法在刘备类的代码中没有被调用,所以它不需要一个包含它基本信息的Methodref
  • 再通俗一点比喻,刘备有技能收买人心,而收买人心技能的发动步骤中包含“大声喊出’摔儿子’三个字,同时发动自己的摔儿子技能”,所以刘备需要像记口诀一样记住“摔儿子”这三个字(即需要在常量池里有这个ref),而因为自己根本不会有喊出“收买人心”四个字的机会,所以常量池里没有必要有“收买人心”的ref。

4 争天下也要排练

上面终于搞懂了我们的台本(类信息)的内容,我也终于理解了罗贯中老导演写的代码到底是什么意思了。舞台剧快开始了,大家赶紧排练(类加载)吧。

加载

排练的第一步,我们每个演员总得拿到我们各自的台本(类信息)吧?

加载的过程,就是将台本纸稿(class文件)的内容导入到方法区大屏幕上的过程,这样我们每个人在排练的时候就可以像看提词器一样,偷瞄我们的设定。

通过一个台本(类)的名称(全限定名),将所有需要的台本文稿(class文件)内容导入到大屏幕,可以使用的导入方式有:

  • 目前可以从zip包获取,即jar,ear,war格式的基础。
  • 从网络获取,即applet实现。
  • 运行时计算生成,典型如动态代理。
  • 由其他文件生成,典型如JSP应用,即为JSP文件生成的class类。
  • 从数据库中读取,这种较少见。

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。并没有明确存放于要在堆中,实际上它虽然是对象,但是HotSpot虚拟机仍将其存放在方法区中。

验证

验证是为了确保台本信息符合这个舞台剧的需求(确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全),一场三国的舞台剧,你不可以乱入一个李逵吧!

验证会检查格式,规范,引用的验证。

准备

方法区大屏幕上已经显示出我们导入的台本内容了,我们可以看到上面写着刘备的一些信息,假设他有个“织席贩履”的设定(类变量),即 static String sheDing=“织席贩履”。那么我们要把这四个字摆到显眼的地方去,因为他是人尽皆知的设定(类变量),表演中被引用到的概率还是很高的。(实例变量不会在此时分配内存)

所以在方法区大屏幕找一个地方(分配内存),但是注意,只是留了一块空间给它,但是还没有将“织席贩履
四个字给写上去,所以它还只是初始值。

基本数据类型的初始值有这些

解析

我们之前介绍过,在台本里面存储的很多都是符号引用(全限定名,简单名称,描述符),比如我们只知道张飞和关羽这两个人的名字,类型而已,并不知道具体对应到哪一个演员。

解析就是将台本上的符号引用,跟真正的演员对应起来的过程。(虚拟机将常量池内的符号引用替换为直接引用的过程)

我们来分析一下刘备的解析过程:

首先,类加载器加载LiuBei这个类的信息。

然后,我们根据台本,知道刘备有“给我上”这个技能(真实解析顺序并非如此,但这里只是示例,逻辑是相通的)。

1
2
3
4
public void geiWoShang(){//给我上
System.out.printf(zhangFei+"");
guanYu.toString();//嘛,就让关张二人随便丢了个技能,Object类嘛,就toString一下。
}

geiWoShang方法的完整信息,存在方法表集合中(记录了各种修饰符,字段类型,和字段名称,以及各种属性)。

我们看上文的反编译信息可以看到,常量池中存在shuaiErZi方法的Methodref,那么同样是刘备的方法,为什么常量池中没有geiWoShang方法的Methodref呢?我们要记住,只有作为字节码参数的目标(方法,或者字段),才有必要在常量池中放置他们的引用。shuaiErZi方法被shouMaiRenXin方法引用,所以有shuaiErZi方法的Methodref。

方法表长这样:

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 void geiWoShang();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class java/lang/StringBuilder
6: dup
7: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
10: aload_0
11: getfield #2 // Field zhangFei:I
14: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
17: ldc #10 // String
19: invokevirtual #11 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: iconst_0
26: anewarray #3 // class java/lang/Object
29: invokevirtual #13 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
32: pop
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
41: return
LineNumberTable:
line 13: 0
line 14: 33
line 15: 41
LocalVariableTable:
Start Length Slot Name Signature
0 42 0 this Lcom/company/LiuBei;

其中有一个属性叫做code,里面的内容就是方法体代码的字节码,它长这样:

1
2
3
4
5
6
 ...
33: aload_0
34: getfield #4 // Field guanYu:Ljava/lang/Object;
37: invokevirtual #14 // Method java/lang/Object.toString:()Ljava/lang/String;
40: pop
...

是的,我们忽略其他,只看调用了guanYu.toString();来作为例子。

getfield #4表示将常量池第四项压入栈。好,常量池第4项还没被解析,那么我们要向解析geiWoShang方法的code,就得先解析常量池第四项。

常量池第四项是啥呢,是个Fieldref,对,是关羽这个字段的Fieldref。

结构抽象后大概是这样:

1
2
3
4
5
6
7
8
9
10
Fieldref{
Class{
index="com/company/LiuBei";index指向的是常量池的 #15=com/company/LiuBei;,表示关羽字段是属于LiuBei类的字段。
}

NameAndType{
name_index="guanYu";//字段表,关羽这个字段,指向的是常量池的#18=guanYu
descriptor_index="Ljava/lang/Object";//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示关羽字段的类型是object类
}
};

解析字段,前提是它所属的类要被加载,我们根据Fieldref的Class_info知道他所属的类是LiuBei,这个已经加载过了,那忽略。(否则,就进入了别的类的加载过程,即用刘备类的加载器区加载别的类。)

然后根据Fieldref的name_index和descriptor_index得到该字段的名称和描述符,去所属类LiuBei的字段表中寻找名称和描述符完全一致的字段。好,找到了:

1
2
3
java.lang.Object guanYu;//字段表,关羽这个字段,name_index指向的是常量池的#18=guanYu
descriptor: Ljava/lang/Object;//descriptor_index指向的是常量池的 #19=Ljava/lang/Object;,表示类型是object类
flags:

那么把关羽这个字段表在刘备类中的偏移量当做直接引用,覆盖常量池的第四项,即#4=关羽这个字段表在刘备类中的偏移量,关羽字段解析完毕,做个标记,解析完成。

这样下次执行getfield #4时,#4直接指向了关羽字段表的直接引用。

同理,我们接下来解析invokevirtual #14,表示调用#14指向的示例方法。

常量池中#14 = Methodref长这样

1
2
3
4
5
6
7
8
9
10
Methodref{
Class{
index="java/lang/Object";index指向的是常量池的 #3=java/lang/Object;,表示该方法是Object的方法。
}

NameAndType{
name_index=" toString";//指向的是常量池的#53=toString,表示名称
descriptor_index="()Ljava/lang/String";//descriptor_index指向的是常量池的 #54=()Ljava/lang/String;,表示方法描述符
}
}

同理,先解析所属的Object类,哦,也加载过了。

那么根据名称和描述符,去Object类的方法表中找到toString方法的偏移量,然后赋值给常量池第十四项。

以此类推,完成所有类的符号引用向直接引用的转变。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程。给类变量赋初值,此时“织席贩履”可以赋值在之前留出的空间上了。

0%