万字长文,完全虚构。(12000字)
( 一 )
组里来了个实习生,李大胖面完之后,觉得水平一般,但还是留了下来,为什么呢? 各自猜去吧。
李大胖也在心里开导自己,学生嘛,不能要求太高,只要肯上进,慢慢来。 就称呼为小白吧。
小白每天来的很早,走的很晚,都在用功学习,时不时也向别人请教。 只是好像天资差了点。
都快一周了,才能写些“简单”的代码,一个注解,一个接口,一个类,都来看看吧:
public @interface Health { String name() default ""; } public interface Fruit { String getName(); void setName(String name); int getColor(); void setColor(int color); } @Health(name = "健康水果") public class Apple implements Fruit { private String name; private int color; private double weight = 0.5; @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } @Override public int getColor() { return color; } @Override public void setColor(int color) { this.color = color; } public double weight() { return weight; } public void weight(double weight) { this.weight = weight; } }
与周围人比起来,小白进步很慢,也许是自己不够聪明,也许是自己不适合干这个,小白好像有点动摇了。
这几天,小白明显没有一开始那么上进了,似乎有点想放弃,这不,趴在桌子上竟然睡着了。
(二)
在梦中,小白来到一个奇怪又略显阴森的地方,眼前有一个破旧的小房子,从残缺不全的门缝里折射出几束光线。
小白有些害怕,但还是镇定了下,深呼吸几口,径直朝着小房子走去。
小白推开门,屋里没有人。 只有一个“机器”在桌子旁大口大口“吃着”东西,背后也不时的“拉出”一些东西。
小白很好奇,就凑了上去,准备仔细打量一番。
“你要干嘛,别影响我工作”。 突然冒出一句话,把小白吓了一大跳,慌忙后退三步,妈呀,心都快蹦出来了。
“你是谁呀? ”,惊慌中小白说了句话。
“我是编译器”,哦,原来这个机器还会说话,小白这才缓了过来。
“编译器”,小白好像听说过,但一时又想不起,于是猜测到。
“网上评论留言里说的小编是不是就是你啊”?
“你才是呢”,编译器白了一眼,没好声气的说到。
要不是看在长得还行的份上,早就把你赶走了,编译器心想。
“哦,我想起来了,编译器嘛,就是编译代码的那个东西”,小白恍然大悟到。
“请注意你的言词,我不是个东西,哦,不对,我是个东西,哦,好像也不对,我。 。 我。 。 ”,编译器自己也快晕了。
编译器一脸的无奈,遇上这样的人,今天我认栽了。
小白才不管呢,心想,今天我竟然见到了编译器,我得好好请教请教他。
那编译器会帮助她吗?
(三)
小白再次走上前来,定睛一看,才看清楚,编译器吃的是Java源码,拉的是class(字节码)文件。
咦,为啥这个代码这么熟悉呢,不就是我刚刚写的那些。 “停,停,快停下来了”。 编译器被小白叫停了。
“你又要干嘛啊”? 编译器到。
“嘻嘻,这个代码是我写的,我想看看它是怎么被编译的”,小白到。
编译器看了看这个代码,这么“简单”,她绝对是个菜鸟。 哎,算了,还是让她看看吧。
不过编译器又到,“整个编译过程是非常复杂的,想要搞清楚里面的门道是不可能的,今天也就只能看个热闹了”。
“编译后的内容都是二进制数据,再通俗点说,就是一个长长的字节数组(byte[])”,编译器继续说,“通常把它写入文件,就是class文件了”。
“但这不是必须的,也可以通过网络传到其它地方,或者保存在内存中,用完之后就丢弃”。
“哇,还可以这样”,小白有些惊讶。 编译器心想,你是山沟里出来的,没见过世面,大惊小怪。
继续到,“从数据结构上讲,数组就是一段连续的空间,是‘没有结构’的,就像一个线段一样,唯一能做的就是按索引访问”。
小白到,“编译后的内容一定很繁多,都放到一个数组里面,怎么知道什么东西都在哪呢? 不都乱套了嘛”。
编译器觉得小白慢慢上道了,心里有一丝安慰,至少自己的讲解不会完全白费。 于是继续到。
“所以JVM的那些大牛们早就设计好了字节码的格式,而且还把它们放入到了一个字节数组里面”。
小白很好奇到,“那是怎么实现的呢”?
“其实也没有太高深的内容,既然数组是按位置的,那就规定好所有内容的先后顺序,一个接一个往数组里放呗”。
“如果内容的长度是固定(即定长)的,那最简单,直接放入即可”。
“如果内容长度是不固定(即变长)的,也很简单,在内容前用一到两个字节存一下内容的长度不就OK了”。
(四)
“字节码的前4个字节必须是一个固定的数字,它的十进制是3405691582,大部分人更熟悉的是它的十六进制,0xCAFEBABE”。
“通常称之为魔术数字(Magic),它主要是用来区分文件类型的”,编译器到。
“扩展名(俗称后缀名)不是用来区分文件类型的吗”? 小白说到,“如.java是Java文件,.class是字节码文件”。
“扩展名确实可以区分,但大部分是给操作系统用的,或给人看到。 如我们看到.mp3时知道是音频、.mp4是知道是视频、.txt是文本文件”。
“操作系统可以用扩展名来关联打开它的软件,比如.docx就会用word来打开,而不会用文本文件”。 编译器继续到。
“还有一个问题就是扩展名可以很容易被修改,比如把一个.java手动改为.class,此时让JVM来加载这个假的class文件会怎样呢”?
“那JVM先读取开头4个字节,发现它不是刚刚提到的那个魔数,说明它不是合法的class文件,就直接抛异常呗”,小白说到。
“很好,真是孺子可教”,编译器说道,“不过还有一个问题,不知你是否注意到? 4个字节对应Java的int类型,int类型的最大值是2147483647”。
“但是魔数的值已经超过了int的最大值,那怎么放得下呢,难道不会溢出吗”?
“确实啊,我怎么没发现呢,那它到底是怎么放的呢”? 小白到。
“其实说穿了不值得一提,JVM是把它当作无符号数对待的。 而Java是作为有符号数对待的。 无符号数的最大值基本上是有符号数最大值的两倍”。
“接下来的4个字节是版本号,不同版本的字节码格式可能会略有差异,其次在运行时会校验,如JDK8编译后的字节码是不能放到JDK7上运行的”。
“这4个字节中的前2个是次(minor)版本,后2个是主(major)版本”。 编译器继续到,“比如我现在用的JDK版本是1.8.0_211,那次版本就是0,主版本就是52”。
“所以前8个字节的内容是,0xCAFEBABE,0,52,它们并不是源代码里的内容”。
Magic [getMagic()=0xcafebabe] MinorVersion [getVersion()=0] MajorVersion [getVersion()=52]
(五)
当编译器读到源码中的public class的时候,然后就就去查看一个表格,如下图:
自顾自的说着,“public对应的是ACC_PUBLIC,值为0x0001,class默认就是,然后又读ACC_SUPER的值0x0020”。
“最后把它俩合起来(按位或操作),0x0001 | 0x0020 => 0x0021,然后把这个值存起来,这就是这个类的访问控制标志”。
小白这次算是开了眼界了,只是还有一事不明,“这个ACC_SUPER是个什么鬼”?
编译器解释到,“这是历史遗留问题,它原本表达在调用父类方法时会特殊处理,不过现在已经不再管它了,直接忽略”。
接着读到了Apple,它是类名。 编译器首先要获取类的全名,org.cnt.java.Apple。
然后对它稍微转换一下形式,变成了,org/cnt/java/Apple,“这就是类名在字节码中的表示”。
编译器发现这个Apple类没有显式继承父类,表明它继承自Object类,于是也获取它的全名,java/lang/Object。
接着读到了implements Fruit,说明该类实现了Fruit接口,也获取全名,org/cnt/java/Fruit。
小白说到,“这些比较容易理解,全名中把点号(.)替换为正斜线(/)肯定也是历史原因了。 但是这些信息如何存到数组里呢”?
“把点号替换为正斜线确实是历史原因”,编译器继续到,“这些字符串虽然都是类名或接口名,但本质还是字符串,类名、接口名只是赋予它的意义而已”。
“除此之外,像字段名、方法名也都是字符串,同理,字段名、方法名也是赋予它的意义。 所以字符串是一种基本的数据,需要得到支持”。
“除了字符串之外,还有整型数字,浮点数字,这些也是基本的数据,也需要得到支持”。
因此,设计者们就设计出了以下几种类型,如图:
“左边是类型名称,方便理解,右边是对应的值,用于存储”,编译器继续到。
“这里的Integer/Long/Float/Double和Utf8都是具体保存数据用的,表示整型数/浮点数和字符串。 其它的类型大都是对字符串的引用,并赋予它一定的意义”。
“所以类名首先被存储为一个字符串,也就是Utf8,它的值对应的是1”。 编译器接着到,“由于字符串是一个变长的,所以就先用两个字节存储字符串的长度,接着跟上具体的字符串内容”。
所以字符串的结构就是这样,如图:
“类名字符串的存储数据为,1、18、org/cnt/java/Apple。 第一个字节为1,表明是Utf8类型,第2、3两个字节存储18,表示字符串长度是18,接着存储真正的字符串。 所以共用去1 + 2 + 18 => 21个字节”。
“父类名字符串存储为,1、16、java/lang/Object。 共用去19个字节”。
“接口名字符串存储为,1、18、org/cnt/java/Fruit。 共用去21个字节”。
小白听的不住点头,编译器喘口气,继续讲解。
“字符串存好后,就该赋予它们意义了,在后续的操作中肯定涉及到对这些字符串的引用,所以还要给每个字符串分配一个编号”。
如Apple为#2,即2号,Object为#4,Fruit为#6。
“由于这三个字符串都是类名或接口名,按照设计规定应该使用Class表示,对应的值为7,然后再指定一个字符串的编号即可”。
因此类或接口的表示如下图:
“先用1个字节指明是类(接口),然后再用2个字节存储一个字符串的编号。 整体意思很直白,就是把这个编号的字符串当作类名或接口名”。
“类就表示为,7、#2。 7表示是Class,#2表示类名称那个字符串的存储编号。 共用去3个字节”。
“父类就表示,7、#4。 共用去3个字节。 接口就表示为,7、#6。 共用去3个字节”。
其实这三个Class也分别给它们一个编号,方便别的地方再引用它们。
(六)
“其实上面这些内容都是常量,它们都位于常量池中,它们的编号就是自己在常量池中的索引”。 编译器说到。
“常量池很多人都知道,起码至少是听说过。 但绝大多数人对它并不十分熟悉,因为很少有人见过它”。
编译器继续到,“今天你可算是来着了”,说着就把小白写的类编译后生成的常量池摆到了桌子上。
“这是什么东西啊,这么多,又很奇怪”,小白说到,这也是她第一次见。
ConstantPoolCount [getCount()=46] ConstantPool [ #0 = null #1 = ConstantClass [getNameIndex()=2, getTag()=7] #2 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Apple, getTag()=1] #3 = ConstantClass [getNameIndex()=4, getTag()=7] #4 = ConstantUtf8 [getLength()=16, getString()=java/lang/Object, getTag()=1] #5 = ConstantClass [getNameIndex()=6, getTag()=7] #6 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Fruit, getTag()=1] #7 = ConstantUtf8 [getLength()=4, getString()=name, getTag()=1] #8 = ConstantUtf8 [getLength()=18, getString()=Ljava/lang/String;, getTag()=1] #9 = ConstantUtf8 [getLength()=5, getString()=color, getTag()=1] #10 = ConstantUtf8 [getLength()=1, getString()=I, getTag()=1] #11 = ConstantUtf8 [getLength()=6, getString()=weight, getTag()=1] #12 = ConstantUtf8 [getLength()=1, getString()=D, getTag()=1] #13 = ConstantUtf8 [getLength()=6, getString()=<init>, getTag()=1] #14 = ConstantUtf8 [getLength()=3, getString()=()V, getTag()=1] #15 = ConstantUtf8 [getLength()=4, getString()=Code, getTag()=1] #16 = ConstantMethodRef [getClassIndex()=3, getNameAndTypeIndex()=17, getTag()=10] #17 = ConstantNameAndType [getNameIndex()=13, getDescriptorIndex()=14, getTag()=12] #18 = ConstantDouble [getDouble()=0.5, getTag()=6] #19 = null #20 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=21, getTag()=9] #21 = ConstantNameAndType [getNameIndex()=11, getDescriptorIndex()=12, getTag()=12] #22 = ConstantUtf8 [getLength()=15, getString()=LineNumberTable, getTag()=1] #23 = ConstantUtf8 [getLength()=18, getString()=LocalVariableTable, getTag()=1] #24 = ConstantUtf8 [getLength()=4, getString()=this, getTag()=1] #25 = ConstantUtf8 [getLength()=20, getString()=Lorg/cnt/java/Apple;, getTag()=1] #26 = ConstantUtf8 [getLength()=7, getString()=getName, getTag()=1] #27 = ConstantUtf8 [getLength()=20, getString()=()Ljava/lang/String;, getTag()=1] #28 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=29, getTag()=9] #29 = ConstantNameAndType [getNameIndex()=7, getDescriptorIndex()=8, getTag()=12] #30 = ConstantUtf8 [getLength()=7, getString()=setName, getTag()=1] #31 = ConstantUtf8 [getLength()=21, getString()=(Ljava/lang/String;)V, getTag()=1] #32 = ConstantUtf8 [getLength()=16, getString()=MethodParameters, getTag()=1] #33 = ConstantUtf8 [getLength()=8, getString()=getColor, getTag()=1] #34 = ConstantUtf8 [getLength()=3, getString()=()I, getTag()=1] #35 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=36, getTag()=9] #36 = ConstantNameAndType [getNameIndex()=9, getDescriptorIndex()=10, getTag()=12] #37 = ConstantUtf8 [getLength()=8, getString()=setColor, getTag()=1] #38 = ConstantUtf8 [getLength()=4, getString()=(I)V, getTag()=1] #39 = ConstantUtf8 [getLength()=3, getString()=()D, getTag()=1] #40 = ConstantUtf8 [getLength()=4, getString()=(D)V, getTag()=1] #41 = ConstantUtf8 [getLength()=10, getString()=SourceFile, getTag()=1] #42 = ConstantUtf8 [getLength()=10, getString()=Apple.java, getTag()=1] #43 = ConstantUtf8 [getLength()=25, getString()=RuntimeVisibleAnnotations, getTag()=1] #44 = ConstantUtf8 [getLength()=21, getString()=Lorg/cnt/java/Health;, getTag()=1] #45 = ConstantUtf8 [getLength()=12, getString()=健康水果, getTag()=1] ]
“在常量池前面会用2个字节来存储常量池的大小,需要记住的是,这个大小不一定就是池中常量的个数。 但它减去1一定是最大的索引”。
“因为,常量池中为0的位置(#0)永远不使用,还有Long和Double类型一个常量占2个连续索引(没错,又是历史原因),实际只是用了第1个索引,第2个索引永远空着(参见#18、#19)”。
编译器继续到,“#0是特殊的,用来表示‘没有’的意思,其它地方如果想表达没有的话,可以指向它。 如Object是没有父类的,所以它的父类指向#0,即没有”。
“所以常量都是从#1开始。 可以看看#1到#6的内容,就是刚刚上面讲的”。 编译器说到。
“真是学到不少知识啊”,小白说到,“关于常量池能不能再多讲点”? 编译器只好继续讲。
(七)
“常量池就是一个容器,它里面放了各种各样的所有信息,并且为每个信息分配一个编号(即索引),如果想要在其它地方使用这些信息,直接使用这个编号就行了”。
编译器继续到,“这个常量池在一些语言中也被称为‘符号表’,通过编号来使用的这种方式也被称为‘符号引用’”。
相信很多爱学习的同学对符号表和符号引用这两个词都很熟悉,不管之前是不是真懂,至少现在应该是真的搞懂了。 因为你已经看到了。
“采用这种常量池和常量引用方式的好处其实很多,就说个最容易想到的,就是重复利用,节省空间,便于管理”。 编译器继续说。
“比如一个类里有10个方法,每个方法里都定义一个length的局部变量,那么length这个名字就会出现在常量池里面,且只会出现一次,那10个方法都是对它的引用而已”。
“如果有一个方法的名字也叫length的话,那也是对同一个常量的引用,因为这个length常量只是个字符串数据而已,本身没有明确含义,它的含义来自于引用它的常量”。
“哦,原来如此”,小白开悟到,“‘符号表、符号引用’这些‘高大上’的叫法,不过就是根据索引去列表里获取元素罢了”,哈哈。
编译器看到小白这么开心,就准备抛出一个问题,“打压”一下她。 于是说到。
“常量池看上去和数组/列表非常相似,都是容器且都是基于索引访问的。 为啥常量池只被称为符号表,而不是符号数组或符号列表呢”?
小白自然回答不上来。 编译器继续说,“表的英文单词是Table。 它和数组/列表的唯一区别就是,数组/列表里的元素长度都是固定的。 表里的元素长度是不固定的”。
“常量池中的好几种常量的长度都是变长的,所以自然是表了”。
小白点了点头,心里想,这编译器就是厉害,我这辈子看来都无法达到他的高度了。
编译器继续说到,“字节码的前8个字节存储魔数和版本,接着的2个(9和10)字节存储常量池的大小,后面接着(从11开始)就是整个常量池的内容了”。
“之所以把常量池放这么靠前,是因为后面的所有内容都要依赖它、引用它”。
紧跟在常量池之后的就是这个类的基本信息,如下:
“首先用2个字节存储上面已经计算好的访问控制标志,即0x0021”。
“然后用2个字节存储这个类在常量池中的索引,就是#1”。
“然后用2个字节存储该类的父类在常量池中的索引,就是#3”。
“由于接口可以有多个,所以再用2个字节存储接口的个数,因为只实现了1个接口,所以就存储数字1”。
“接着存储所有接口在常量池中的索引,每个接口用2个字节。 因为只实现了1个接口,所以存储的索引就是#5”。
AccessFlags [getAccessFlags()=0x21, getAccessFlagsString()=[ACC_PUBLIC, ACC_SUPER]] ThisClass [getClassIndex()=1, getClassName()=org/cnt/java/Apple] SuperClass [getClassIndex()=3, getClassName()=java/lang/Object] InterfacesCount [getCount()=1] Interfaces [getClassIndexes()=[5], getClassNames()=[org/cnt/java/Fruit]]
(八)
编译器继续到,“接下来该读取字段信息了”。 当读到private时,就去下面这张表里找:
找到ACC_PRIVATE,把它的值0x0002保存以下,这就是该字段的访问控制标志。
接着读到的是String,这是字段的类型,然后会把这个String类型存入常量池,对应的索引是#8。
可以看到是一个Utf8,说明是字符串,内容是 Ljava/lang/String; ,以大写L开头,已分号;结尾,中间是类型全名,这是在字节码中表示类(对象)类型的方式。
接着读到的是name,这是字段名称,也是个字符串,同样也把它放入常量池,对应的索引是#7。
编译器说到,“现在一个字段的信息已经读取完毕,按照相同的方式把剩余的两个字段也读取完毕”。
“那字段的信息又该怎么存储呢”? 小白问到。“不要着急嘛”,编译器说着就拿出了字段的存储格式:
首先2个字节是访问控制标志,接着2个字节是字段名称在常量池中的索引,接着2个字节是字段描述(即类型)在常量池中的索引。
接着2个字节就是属性个数,然后就是具体的属性信息了。 例如字段上标有注解的话,这个注解信息就会放入属性信息里。
编译器继续说到,“属性信息是字节码中比较复杂的内容,这里就不说太多了”。 接着就可以按格式整理数据了。
因为一个类的字段可以有多个,所以先用2个字节存储一下字段数目,本类有3个字段,所以就存储个3。
第一个字段,0x0002、#7、#8、0。 共用去8个字节,因为自动没有属性内容。
第二个字段,0x0002、#9、#10、0。 共用去8个字节。
第二个字段,0x0002、#11、#12、0。 共用去8个字节。
编译器接着说,“所以存储这3个字段信息共用去2 + 8 + 8 + 8 => 26个字节”。
小白说到,“我现在基本已经搞明白套路了。 其实有些东西没有想象中的那么复杂啊”。
“复杂的东西还是有的,我们现在先不考虑”,编译器说到,“还有一个问题,不知你发现了没有”。
字段color的类型是int,但是在常量池中却变为大写字母I,同样weight的类型是double,常量池中却是大写字母D。
小白说到,“我来猜测一下吧,int、double是Java中的数据类型,I、D是与之对应的在JVM中的表示形式。 对吧”?
“算你聪明”,编译器说到,“其实Java和JVM之间关于类型这块有一个映射表”,如下:
有两个需要注意。 “第一点上面已经说过了,就是类都会映射成LClassName;这种形式,如Object映射为Ljava/lang/Object;”。
第二点是数组,“数组在Java中用一对中括号([])表示,在JVM中只用左中括号([)表示。 也就是[]映射为[”。
“多维数组也一样,[][][]映射为[[[”。 然后还有类型,“Java是把类型放到前面,JVM是把类型放到后面”。 如double[]映射为[D。
“double[][][]映射为[[[D”。 同理,“String[]映射为[Ljava/lang/String;,Object[][]映射为[[Ljava/lang/Object;”。
“我似乎又明白了一些,Java有自己的规范,字节码也有自己的规范,它们之间的映射关系早都已经定义好了”。 小白继续到。
“只要按照这种映射关系,就能把Java源码给转换为字节码。 是吧”?
“粗略来说,可以这么理解,其实这就是编译了,但一定要清楚,真正的编译是非常复杂的一个事情”,编译器到。
小白说到,“字段完了之后,肯定该方法了,就交给我吧,让我也试试”。
“年轻人啊,就是生猛,你来试试吧”。 编译器说到。
FieldsCount [getCount()=3] Fields [ #0 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=7, getName()=name, getDescriptorIndex()=8, getDescriptor()=Ljava/lang/String;, getAttributesCount()=0, getAttributes()=[]] #1 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=9, getName()=color, getDescriptorIndex()=10, getDescriptor()=I, getAttributesCount()=0, getAttributes()=[]] #2 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=12, getDescriptor()=D, getAttributesCount()=0, getAttributes()=[]] ]
(九)
小白说,“方法呢肯定也有自己的格式,你把它找出来我看看”。
“好好,我这就找”,编译器苦笑到。 我堂堂一个编译器,今天竟然成了小白的助手,惭愧啊。
说着编译器就找到了,于是放到了桌子上:
“咦,怎么和字段的一模一样”,小白到。 那这就更简单了。
先是访问控制标志,接着是方法名称索引,然后是方法描述索引,最后是和方法关联的属性。 于是照猫画虎,小白就开始了。
先读到public关键字,这是个访问控制修饰符,肯定也有一张表和它对应,可以找到这个关键字对应的数值。
还没等小白开口,编译器就赶紧把表找出来了:
小白继续,ACC_PUBLIC对应的值是0x0001,就把这个值先保存起来。
然后是方法的名字,getName,是一个字符串,照例把它存入常量池,并且有一个索引,就是#26。
接着该方法的描述了,小白认为方法和字段是不同的,除了有返回类型之外,还有参数呢,这该咋整呢?
于是就问编译器,“方法的描述应该也有格式吧”?
“你越来越聪明了”,编译器说到,“其实也很简单,我来简单说下吧”。
“在Java中如果把访问控制符、方法名、参数名、方法体都去掉,其实就剩下‘方法签名’了”。
例如,没有入参没有返回值的,就是这个样子,void()。
返回值为String,入参为int,double,String的,其实就是这样个子,String(int, double, String)。
“这个方法签名其实就是在Java中对方法的描述,在字节码中和它差不多,就是把返回类型放到后面,把参数间的逗号去掉”。
因此void()映射为()V,这里要注意的是void对应的是大写字母V。
String(int, double, String)映射为(IDLjava/lang/String;)Ljava/lang/String;
“不难,不难”,小白说到,于是又继续开始了。
小白按照这种格式,把刚刚的那个方法描述也存入了常量池,得到的索引就是#27。
小白按这个套路把6个方法都整理好了,接下来该按格式把数据写入字节数组了。
编程新说注: 方法的代码对应的是JVM的指令,这里就忽略不谈了,后续可能会单独再说。
编译器提醒小白说,“你是不是还漏掉了一个方法啊”?
小白又看了一遍Java源码,仔细数了数,是6个呀,没错啊。
编译器说到,你在学习时有没有见过这样一句话,“当类没有定义构造函数时,编译器会为它生成一个默认的无参构造函数”。
小白连忙点头,“嗯嗯嗯,见过的”。
“这就是了”,编译器说道,“不过需要注意的是,在字节码中构造方法的名字都是<init>,返回类型都是V”。
“这也是规定的吧”,小白说到,编译器点了点头。
编译器又说到,“其实还有方法的参数信息,如参数位置,参数类型,参数名称,参数的访问控制标志等”。
“这些信息都是放在方法格式里最后的属性信息中的,咱们也暂时不说它们了”。
编程新说注 :
在JDK7及以前,字节码中不包含方法的参数名。 因为JVM执行指令时,参数是按位置传入的,所以参数名对代码的执行没有用处。
由于越来越多的框架采用按方法参数名进行数值绑定,Java也只好在JDK8时加入了对参数名的支持。
不过需要设置一下编译器的–parameters参数,这样才能把方法参数名也放入字节码中。
可以看看常量池中的#32是“MethodParameters”字符串,说明字节码中已经包含参数名了。
常量池中#7、#9、#11三个字符串就是参数名,同时也是字段名,这就是复用的好处。
编程新说注 : 方法的格式和字段的格式完全一样,就不再演示写入过程了。
因此这个类共有7个方法。
MethodsCount [getCount()=7] Methods [ #0 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=13, getName()=<init>, getDescriptorIndex()=14, getDescriptor()=()V, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=1, getCodeLength()=12, getJvmCode()=JvmCode [getCode()=12], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=3, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=8], LineNumTable [getStartPc()=4, getLineNumber()=12], LineNumTable [getStartPc()=11, getLineNumber()=8]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=12, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]] #1 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=26, getName()=getName, getDescriptorIndex()=27, getDescriptor()=()Ljava/lang/String;, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=16]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]] #2 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=30, getName()=setName, getDescriptorIndex()=31, getDescriptor()=(Ljava/lang/String;)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=21], LineNumTable [getStartPc()=5, getLineNumber()=22]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=7, getDescriptorIndex()=8, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=7, getAccessFlags()=0x0]]]]] #3 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=33, getName()=getColor, getDescriptorIndex()=34, getDescriptor()=()I, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=26]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]] #4 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=37, getName()=setColor, getDescriptorIndex()=38, getDescriptor()=(I)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=31], LineNumTable [getStartPc()=5, getLineNumber()=32]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=9, getDescriptorIndex()=10, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=9, getAccessFlags()=0x0]]]]] #5 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=39, getDescriptor()=()D, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=35]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]] #6 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=40, getDescriptor()=(D)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=3, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=39], LineNumTable [getStartPc()=5, getLineNumber()=40]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=11, getDescriptorIndex()=12, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=11, getAccessFlags()=0x0]]]]] ]
编程新说注 : 方法部分的输出内容很多,是因为包含了方法体的代码的信息。
(十)
“真是后生可畏啊”,编译器感慨到。 “小白竟然也能按照套路去在做点事情了”。
不过编译器并不自危,因为最核心的内容是,可执行代码如何转换为JVM指令集中的指令,这可是“压箱底”的干货,可不能随便告诉别人,长得再好看也不行。 哈哈,O(∩_∩)O。
接着编译器拿出一个完整的字节码文件格式图给小白看:
小白看完后说,“和刚刚讲的一样,只是最后也有这个属性信息啊”。
编译器说,“属性信息是字节码文件中非常复杂的内容,可以暂时不管用了”。
上面已经说了,至少注解的相关内容是放在属性信息里的。
那就看看你写的这个类的属性信息都是什么吧:
AttributesCount [getCount()=2] Attributes [ #0 = SourceFile [getSourcefileIndex()=42] #1 = RuntimeVisibleAnnotations [getNumAnnotations()=1, getAnnotations()=[Annotation [getTypeIndex()=44, getNumElementValuePairs()=1, getElementValuePairs()=[ElementValuePair [getElementNameIndex()=7, getElementValue()=ElementValue [getTag()=ElementValueTag [getTagChar()=s], getUnion()=ElementValueUnion [getConstValueIndex()=45]]]]]]] ]
编译器继续说,共有2条属性信息,第一条是源代码文件的名字,在常量池中的#42。 其实就是Apple.java了。
第二条是运行时可见的注解信息,本类共有1个注解,注解类型是常量池中的#44。 其实就是Lorg/cnt/java/Health;了。
该注解共显式设置了1对属性值。 属性名称是常量池中的#7,就是name了,类型是小写的s,表示String类型,属性值是#45,也就是“健康水果”了。
下图中的这些类型,都是可以用于注解属性的类型:
最后,编译器打印出一行信息:
-----bytes=1085-----
小白说,“这是什么意思”?“这是编译后产生的字节码的总长度,是1085个字节”,编译器到。
小白刚想表达对编译器的感谢,忽然闻到一阵香味,而且是肉香。
PS:最后几句话就不写了,请你来补充完整吧,嘻嘻。
>>> 热门文章集锦 <<<
毕业10年,我有话说
【面试】我是如何面试别人List相关知识的,深度有点长文
我是如何在毕业不久只用1年就升为开发组长的
爸爸又给Spring MVC生了个弟弟叫Spring WebFlux
【面试】我是如何在面试别人Spring事务时“套路”对方的
【面试】Spring事务面试考点吐血整理(建议珍藏)
【面试】我是如何在面试别人Redis相关知识时“软怼”他的
【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)
【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)
【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)
【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生(深度好文,建议珍藏)
【面试】如果把线程当作一个人来对待,所有问题都瞬间明白了
Java多线程通关———基础知识挑战
品Spring:帝国的基石
作者 是工作超过 10年 的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。 追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。 下面是公众号和知识星球的二维码,欢迎关注!