万字长文,完全虚构。

(一)

组里来了个实习生,李大胖面完之后,觉得水平一般,但还是留了下来,为什么呢?各自猜去吧。

李大胖也在心里开导自己,学生嘛,不能要求太高,只要肯上进,慢慢来。就称呼为小白吧。

小白每天来的很早,走的很晚,都在用功学习,时不时也向别人请教。只是好像天资差了点。

都快一周了,才能写些“简单”的代码,一个注解,一个接口,一个类,都来看看吧:

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

【JVM故事】一个Java字节码文件的诞生记的更多相关文章

  1. JAVA字节码文件之结构

    开发工具:IEDA.JDK1.8.WinHex 一.字节码文件结构 源代码 package com.jalja.java.bytecode; /** * @Auther: XL * @Date: 20 ...

  2. JAVA字节码文件之第三篇(访问标识)

    一.Access Flags 访问标志 访问标志信息包括该 Class 文件是类还是接口,是否被定义成 public 或者 abstract , 如果是类,是否被声明成 final. 访问标志表 二. ...

  3. JAVA字节码文件之第四篇(方法分析)

    一.Methods 方法字节码结构 Methods 字节码结构: Methods num:占两byte,Methods 的具体内存占n个byte 方法中每个属性都是Attribute_info,Att ...

  4. [置顶] Java字节码文件剖析

    Java为什么能够支持跨平台,其实关键就是在于其*.class字节码文件,因为*.class字节码文件有一个统一标准的规范,里面是JVM运行的时需要的相关指令,各家的JVM必须能够解释编译执行标准字节 ...

  5. JAVA字节码文件之常量池

    一.常量池的内容 一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如java类中定义的方法与变量信息.常量池中主要存储两类常量:字面量(文本字符 ...

  6. 命令行中运行Java字节码文件提示找不到或无法加载主类的问题

    测试类在命令行操作,编译通过,运行时,提示 错误: 找不到或无法加载主类 java类 package com.company.schoolExercise; public class test7_3_ ...

  7. 【java】查看Java字节码文件内容的方法+使用javap找不到类 解决方法

    研究synchronized底层实现,涉及到查看java字节码的需要 前提是,你的PC已经成功安装了JDK并别配置了环境变量. ==========查看方法========= 一.javap查看简约字 ...

  8. java字节码文件指令集

    网上找的没有指令码这列  自己把它加上 更方便查阅 指令从0x00-0xc9 没有0xba 常量入栈指令 指令码 操作码(助记符) 操作数 描述(栈指操作数栈) 0x01 aconst_null nu ...

  9. JAVA反射机制_获取字节码文件对象

    是在运行状态中,对于任意一个类 (class文件),都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性: 这种动态获取的信息以及动态调用对象的方法的功能称为java语 ...

随机推荐

  1. iNeuOS工业互联平台,实现动态图元、计算平台、远程控制、数据转发等,和大厂相比如何

    目       录 1.      概述... 2 2.      平台演示... 2 3.      增加按钮组态元件... 2 4.      组态图元旋转及动画... 3 5.      后台容 ...

  2. PAT 1006 Sign In and Sign Out (25分) 字符串比较

    题目 At the beginning of every day, the first person who signs in the computer room will unlock the do ...

  3. React面试题汇总

    1.如何理解React中的组件间数据传递? ①父-子  通过props传递 ②子-父  在父中创建一个可以修改state的方法,之后把这个方法通过props传递给子,在子中调用这个方法 从而达到修改父 ...

  4. 安装OPENCTI

    应业务需求,需要安装OPENCTI.很无奈的配了一下午. 首先是安装需求: 1. Ubuntu 2. Docker version 19.03.5 + docker-compose version 1 ...

  5. VMware 11安装Mac OS X 10.10 (转载)

    VM11安装Mac OS X 10.10 工具/原料 1.VMware Workstation 112.unlocker 203(for OS X 插件补丁)3.Mac OS X 10.10镜像方法/ ...

  6. vue修改对象的属性值后页面不重新渲染

    原文地址:vue修改对象的属性值后页面不重新渲染 最近项目在使用vue,遇到几次修改了对象的属性后,页面并不重新渲染,场景如下: HTML页面如下: [html] view plain copy &l ...

  7. 两圆相交求面积 hdu5120

    转载 两圆相交分如下集中情况:相离.相切.相交.包含. 设两圆圆心分别是O1和O2,半径分别是r1和r2,设d为两圆心距离.又因为两圆有大有小,我们设较小的圆是O1. 相离相切的面积为零,代码如下: ...

  8. JavaScript数组常见用法

    最近做一个项目中做一个竞猜游戏界面,游戏规则和彩票是一样的.在实现“机选一注”,“机选五注”的时候遇到数组的一些操作,例如产生['01', '02' ... '35']这样的数组,随机抽取不重复的元素 ...

  9. Java连接MySql报错—— com.mysql.cj.exceptions.InvalidConnectionAttributeException

    详细报错 java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents mor ...

  10. [工具-004]如何从apk中提取AndroidManifest.xml并提取相应信息

    跟上一篇类似,我们也需要对APK的一些诸如umengkey,ADkey,TalkingData进行验证,那么我们同样需要解压apk文件,然后提取其中的AndroidManifest.xml.然后解析x ...