作者:李新杰·转自微:信公众号“编程新说”
万字长文,完全虚构。(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。
<