1 我乃中山靖王之后
大家好,我叫李大锤,是一名不入流的演员,我即将参演一部名叫《三国演义》的舞台剧,导演是棺材板按不住的罗贯中老先生。而我,即将扮演三位主角之一的刘皇叔,嘿嘿,想想还有点小激动呢!
按照剧本,我是一名出生低微的屌丝,被嘲笑为“织席贩履”之辈,所以一开始,我长这个B样:
1 | public class LiuBei { |
然而,我开局就会收关张两位挂逼做小弟,于是,我变成了:
1 | public class LiuBei { |
算了,就不追求麾下武将如云谋士如雨了,人太多写的也累,有二弟和三弟出场就够了。
当然,作为未来的汉昭烈帝,我开局还会一些特殊技能,不亏是主角之一,这技能真是别具一格:
1 | public void shuaiErZi() {//摔儿子 |
好了,身残志坚,躺在棺材里还在coding的著名程序员罗贯中先生,已经通过他精湛的代码功底,为我编写了一个详(jian)细(lou)的开局设定:
1 | public class LiuBei { |
现在,让我们去为舞台剧做一下准备吧!
2 新手村
舞台剧开演在即,来,摄影机往前,我们先来俯瞰一下整个会场的布局吧(详细介绍见:JAVA内存结构和内存管理):
首先,面积最为广大的,就是我们舞台的后台,我们唤作堆,所有有名有幸的三国豪杰们(对象们),都会在后台齐聚,各自准备。
然后,我们可以看到一块巨大的显示屏,我们唤作方法区,上面是本剧的台本,上面写着:
- 各个英雄豪杰的设定/经历(类信息)等信息
- 刘备会遇到关张,然后还会摔儿子技能(属性,方法)
- 曹操麾下有曹仁曹纯夏侯兄弟等挂逼,还有好人妻这个技能。(属性,方法)
- ….
- 一些人尽皆知的信息(常量)
- 比如现在是东汉末年,嗯,比如东汉末年是个常量。
- …
- 某位英雄广为人知的设定(类的静态变量)。
- 刘备:说织席贩履的给老子滚出来啊魂淡!!
- 曹操:梦中杀伦什么的,我真不是故意的。
- 孙权:就不能不提合肥,不提孙十万吗。。
舞台之上,我们看到了有三束聚光灯各自照亮舞台一隅,这是以我们三位主角曹孙刘为视角的三个线程,然后被聚光灯照亮的三块方寸之地,主要是虚拟机栈和本地方法栈,还有一个小的牌子,叫做程序计数器,用来标记此间主角演到剧情的何处了。
3 英雄要问出身
逛完了舞台以后,我得去看看我的台本,虽然我在接戏之前已经知道了罗贯中老先生为我量身定做的草稿:
1 | public class LiuBei { |
但草稿只是草稿,正经的舞台剧,肯定不能用这么简陋的东西来演出,不说别的,看草稿我只知道我有关张两个小弟,但演出时,我至少得知道关张是谁来演,我到底和谁撘对手戏吧?是胡歌还是霍建华?
所以,还需要把草稿再加工,变成真正的台本,这个过程,叫做编译,这时,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 | Classfile /home/lisheng/IdeaProjects/learning/out/production/learning/com/company/LiuBei.class |
看完了上面的反解析内容,我们要明白:方法表和常量池里面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 | public void geiWoShang(){//给我上 |
geiWoShang方法的完整信息,存在方法表集合中(记录了各种修饰符,字段类型,和字段名称,以及各种属性)。
我们看上文的反编译信息可以看到,常量池中存在shuaiErZi方法的Methodref,那么同样是刘备的方法,为什么常量池中没有geiWoShang方法的Methodref呢?我们要记住,只有作为字节码参数的目标(方法,或者字段),才有必要在常量池中放置他们的引用。shuaiErZi方法被shouMaiRenXin方法引用,所以有shuaiErZi方法的Methodref。
方法表长这样:
1 | public void geiWoShang(); |
其中有一个属性叫做code,里面的内容就是方法体代码的字节码,它长这样:
1 | ... |
是的,我们忽略其他,只看调用了guanYu.toString();
来作为例子。
getfield #4
表示将常量池第四项压入栈。好,常量池第4项还没被解析,那么我们要向解析geiWoShang方法的code,就得先解析常量池第四项。
常量池第四项是啥呢,是个Fieldref,对,是关羽这个字段的Fieldref。
结构抽象后大概是这样:
1 | Fieldref{ |
解析字段,前提是它所属的类要被加载,我们根据Fieldref的Class_info知道他所属的类是LiuBei,这个已经加载过了,那忽略。(否则,就进入了别的类的加载过程,即用刘备类的加载器区加载别的类。)
然后根据Fieldref的name_index和descriptor_index得到该字段的名称和描述符,去所属类LiuBei的字段表中寻找名称和描述符完全一致的字段。好,找到了:
1 | java.lang.Object guanYu;//字段表,关羽这个字段,name_index指向的是常量池的#18=guanYu |
那么把关羽这个字段表在刘备类中的偏移量当做直接引用,覆盖常量池的第四项,即#4=关羽这个字段表在刘备类中的偏移量,关羽字段解析完毕,做个标记,解析完成。
这样下次执行getfield #4
时,#4直接指向了关羽字段表的直接引用。
同理,我们接下来解析invokevirtual #14
,表示调用#14指向的示例方法。
常量池中#14 = Methodref长这样
1 | Methodref{ |
同理,先解析所属的Object类,哦,也加载过了。
那么根据名称和描述符,去Object类的方法表中找到toString方法的偏移量,然后赋值给常量池第十四项。
以此类推,完成所有类的符号引用向直接引用的转变。
初始化
初始化阶段是执行类构造器<clinit>()方法的过程。给类变量赋初值,此时“织席贩履”可以赋值在之前留出的空间上了。