【JVM故事】一个Java字节码文件的诞生记

万字长文,完全虚构。(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年 的码农,现在任架构师。喜欢研究技术,崇尚简单快乐。 追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。 下面是公众号和知识星球的二维码,欢迎关注!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章