【自己动手写JVM】03.类文件与寻路之旅(原理)

146 阅读6分钟

概览

1.类文件是啥,里面存放着什么

2.如何找到类文件并进行读取

涉及内容

java语言类型、class文件内容、类路径、类的全限定名、双亲委派

类文件是个什么东东

学过java的都知道,java是混合型语言,先由java编译器(另一个手写作品,无限烂尾中~)编译成字节码(编译),然后由jvm运行(解释执行)。所以这些以.class结尾的文件,里面存放着编译后一串一串字节码。如果要运行程序,就绕不开.class文件。

image.png

点开一个class文件,映入眼帘的是一串串数字和字母。啥啥啥这写的都是啥?!
好吧,这些都是16进制的字节码,每个字节码代表1~f(2的4次方),两个字节码就是一字节,是给机器看的,我们当然看不懂。好在我们现在只要找到这些文件,读取出里面的内容就行了。

(btw,当时想过为啥是16进制而不是8或32进制。用2的8次方表示一个字节当然很好,但是凑不出这么多符号。字符越少存放效率越高,所以把一个字节切两半,每一半8/2=4位(也就是2的4次方,16进制)当然是最好的啦)

image.png

类文件寻路之旅

知道了为什么要找类文件后,那么我们该去哪里找它呢。

硬盘那么大,你丫在哪(classpath的由来)

电影《卡萨布兰卡》中有句台词“世上有很多城镇,城镇有很多酒馆”。对于我们的电脑来说,就是电脑上有很多文件夹,文件夹里面又有很多文件夹 ... 对于jvm来说,肯定不能满硬盘去找类文件啦
就跟平时找人知道可能出现的几个地方外,jvm也会根据一些设定好的路径去寻找class文件,这些路径也就是环境变量中所谓的classpath(类路径)
那jvm遍历classpath下所有的目录,找到xxx.class的文件就好了,吗?
当然不。除了扩展名为class的文件外,jarziprar这些压缩文件里面也可能有。不过我们不管其他,去jar里面找就行了。好在jar基本可以当成压缩文件。用第三方库进行解析就成了(这个库找了我好久好久啊!!!落泪

我们都叫约翰,请问你找哪位(全限定类名)

以前看历史书的时候总在想,为什么欧洲人总喜欢在名字前加个地名或者其他奇怪的外号,比如长腿爱德华、征服者威廉、无地王约翰。后来明白了叫这个名字的太多,得加点东西加以区分。

想想以前开发的时候,由于捉急的英语水平,10个类恨不能有8个重名。比如小学生,中学生,大学生,名字都叫Student,但是在不同的包下而已。后来发现,连jdk也会出现这种问题,比如在rt.jar(rt即runtime)这个jdk的核心包下,搜索出了两个都叫Object的类文件:

image.png

这就令人尴尬了。好家伙jvm哼哧哼哧遍历完了classpath,发现这里有个Object、那里也有个Object,真是让人想起河神的经典问题————你掉的是这把金斧头、银斧头、还是这把铁斧头呢。不过jvm可不会回答河神这个问题,说不好还报一大堆错误给河神看。

为了解决这个问题,我们通常会使用全限定类名进行解决。所谓全限定类名,就是类的包名+类的文件名。就像你直接大喊一声“约翰”这个名字可能十个有五个有回应。不过如果你加上限定如“铁匠约翰”、“来自汉诺威的约翰”,就会好很多。比如上面那两个Object,用java.lang.Object和org.omg.CORBA.Object就能区分开。
至于怎么区分,可以用类文件的后缀名称区分(比如java.lang.Object的类文件结尾必定为java/lang/Object.class,因为包就相当于一个文件夹嘛),也可以解析类文件后从里面获取。当然现在还没到解析类的步骤,而且相比于前者,解析类文件效率还是太低了,因此推荐采用第一种。
通过类的全限定类名,虽然不能完全避免重名,但也已经大大降低了重名的概率,而且java project会有几乎独一无二groupId和artifactId

所有的类路径都是平等的,但有些类路径比别的更平等(双亲委派)

通过全限定类名,我们可以大大降低重名概率了。不过在寻找类文件的路上,还是会有如下这些问题:
(1)两个类路径a和b,a的类文件使用频率比b要低(比如a是我写的样例,b是核心类库rt.jar),那jvm先查找a再查找b明显会造成效率浪费嘛
(2)类路径a是我的样例代码,类路径b存放rt.jar。现在我雄心壮志打算重构类库(大雾),写了个java.lang.Object,但是还没完工。如果jvm先扫描a,把我那个残缺不齐的java.lang.Object加载进去了,运行时会出大问题滴
(3)为了实现特殊功能,通过继承的关系给加载器a写了个子类加载器b。可能会导致同一个类,b加载了一次,然后a还继续加载。
为了解决如上的问题,双亲委派(吐槽一下这个翻译)机制应运而生(当然java规范没有)。将类路径划分为三六九等,不同等级的类使用不同的优先级别的加载器进行加载。
加载顺序如下图,加载器委托更高级别加载器的进行加载(根据设定的类路径搜索)。若更高级别的加载器加载类失败,本级别的加载器开始查找。找不到,则更低级别的加载器进行加载,直到最后最低级的加载器也加载失败,抛出万恶的ClassNotFound异常。嗯,这就是传说中的责任链模式吧!
责任链模式的好处大大滴,第一是避免类的重复加载,第二是避免了核心类被篡改(比如刚才的第三个问题),不过我认为还有优化了类文件的查找速率

双亲委派机制定义了如下级别的加载器,本人的jvm也是按照此进行开发滴~
1.启动类加载器(BootstrapClassLoader):加载最jdk最核心的类库。一般是java目录\jre\lib(你喜欢,也可以换个路径),著名的rt.jar就在里面
2.扩展类加载器(ExtensionClassLoader):加载一些jdk扩展的类库。一般是java目录\jre\ext,我的样例程序就放在里面
3.应用类加载器(ApplicationClassLoader):我jvm里面最低级的类加载器了,当然还有更第一点的自定义类加载器
4.用户自定义加载器

当然啦,我们的jvm实现前三个类加载器——启动、扩展和应用即可 image.png

总结

通过以上内容,知道了为什么要以及要如何找类文件,以及类加载器的作用:根据类的全限定名,在不同的优先级的类路径里找到该类文件

至于代码的具体实现,留到下一篇文章开讲!