为了搞清楚类加载,竟然手撸 JVM!

作者:小傅哥
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、前言
学习,不知道从哪下手?
当学习一个新知识不知道从哪下手的时候,最有效的办法是梳理这个知识结构的脉络信息,汇总出一整张的思维导出。接下来就是按照思维导图的知识结构,一个个学习相应的知识点,并汇总记录。
就像 JVM 的学习,可以说它包括了非常多的内容,也是一个庞大的知识体系。例如:类加载、加载器、生命周期、性能优化、调优参数、调优工具、优化方案、内存区域、虚拟机栈、直接内存、内存溢出、元空间、垃圾回收、可达性分析、标记清除、回收过程等等。如果没有梳理的一头扎进去,东一榔头西一棒子,很容易造成学习恐惧感。
如图 24-1 是 JVM 知识框架梳理,后续我们会按照这个结构陆续讲解每一块内容。
二、面试题
谢飞机,小记!,很多知识根本就是背背背,也没法操作,难学!
谢飞机:大哥,你问我两个 JVM 问题,我看看我自己还行不!
面试官:啊?嗯!往死了问还是?
谢飞机:就就就,都行!你看着来!
面试官:啊,那 JVM 加载过程都是什么步骤?
谢飞机:巴拉巴拉,加载、验证、准备、解析、初始化、使用、卸载!
面试官:嗯,背的挺好!我怀疑你没操作过! 那加载的时候,JVM 规范规定从第几位开始是解析常量池,以及数据类型是如何定义的,u1、u2、u4,是怎么个玩意?
谢飞机:握草!算了,告诉我看啥吧!
三、类加载过程描述
JVM 类加载过程分为,加载、链接、初始化、使用和卸载这四个阶段,在链接中又包括:验证、准备、解析。
加载:Java 虚拟机规范对 class 文件格式进行了严格的规则,但对于从哪里加载 class 文件,却非常自由。Java 虚拟机实现可以从文件系统读取、从 JAR(或 ZIP)压缩包中提取 class 文件。除此之外也可以通过网络下载、数据库加载,甚至是运行时直接生成的 class 文件。
链接:包括了三个阶段;
- 验证,确保被加载类的正确性,验证字节流是否符合 class 文件规范,例魔数 0xCAFEBABE,以及版本号等。
- 准备,为类的静态变量分配内存并设置变量初始值等
- 解析,解析包括解析出常量池数据和属性表信息,这里会包括 ConstantPool 结构体以及 AttributeInfo 接口等。
初始化:类加载完成的最后一步就是初始化,目的就是为标记常量值的字段赋值,以及执行
<clinit>方法的过程。JVM 虚拟机通过锁的方式确保 clinit 仅被执行一次使用:程序代码执行使用阶段。
卸载:程序代码退出、异常、结束等。
四、写个代码加载下
JVM 之所以不好掌握,主要是因为不好实操。虚拟机是 C++ 写的,很多 Java 程序员根本就不会去读,或者读不懂。那么,也就没办法实实在在的体会到,到底是怎么加载的,加载的时候都干了啥。只有看到代码,我才觉得自己学会了!
所以,我们这里要手动写一下,JVM 虚拟机的部分代码,也就是类加载的过程。通过 Java 代码来实现 Java 虚拟机的部分功能,让开发 Java 代码的程序员更容易理解虚拟机的执行过程。
1. 案例工程
以上,工程结构就是按照 JVM 虚拟机规范,使用 Java 代码实现 JVM 中加载 class 文件部分内容。当然这部分还不包括解析,因为解析部分的代码非常庞大,我们先从把 .class 文件加载读取开始了解。
2. 代码讲解
2.1 定义类路径接口(Entry)
接口中提供了接口方法
readClass和静态方法create(String path)。jdk1.8 是可以在接口中编写静态方法的,在设计上属于补全了抽象类的类似功能。这个静态方法主要是按照不同的路径地址类型,提供不同的解析方法。包括:CompositeEntry、WildcardEntry、ZipEntry、DirEntry,这四种。接下来分别看每一种的具体实现
2.2 目录形式路径(DirEntry)
目录形式的通过读取绝对路径下的文件,通过
Files.readAllBytes方式获取字节码。
2.3 压缩包形式路径(ZipEntry)
其实压缩包形式与目录形式,只有在文件读取上有包装差别而已。
FileSystems.newFileSystem
2.4 混合形式路径(CompositeEntry)
File.pathSeparator,是一个分隔符属性,win/linux 有不同的类型,所以使用这个方法进行分割路径。分割后的路径装到 List 集合中,这个过程属于拆分路径。
2.5 通配符类型路径(WildcardEntry)
这个类属于混合形式路径处理类的子类,唯一提供的方法就是把类路径解析出来。
2.6 类路径解析(Classpath)
启动类路径、扩展类路径、用户类路径,熟悉吗?是不经常看到这几句话,那么时候怎么实现的呢?
有了上面我们做的一些基础类的工作,接下来就是类解析的实际调用过程。代码如下:
启动类路径,bootstrapClasspath.readClass(className);
扩展类路径,extensionClasspath.readClass(className);
用户类路径,userClasspath.readClass(className);
这回就看到它们具体在哪使用了吧!有了具体的代码也就方便理解了
2.7 加载类测试验证
这段就是使用 Classpath 类进行类路径加载,这里我们测试加载 java.lang.String 类。你可以加载其他的类,或者自己写的类
配置 IDEA,program arguments 参数:
-Xjre "C:\Program Files\Java\jdk1.8.0_161\jre" java.lang.String另外这里读取出的 class 文件信息,打印的是 byte 类型信息。
测试结果
这块部分截取的程序运行打印结果,就是读取的 class 文件信息,只不过暂时还不能看出什么。接下来我们再把它翻译过来!
五、解析字节码文件
JVM 在把 class 文件加载完成后,接下来就进入链接的过程,这个过程包括了内容的校验、准备和解析,其实就是把 byte 类型 class 翻译过来,做相应的操作。
整个这个过程内容相对较多,这里只做部分逻辑的实现和讲解。如果读者感兴趣可以阅读小傅哥的《用Java实现JVM》专栏。
1. 提取部分字节码
java.lang.String 解析出来的字节码内容较多,当然包括的内容也多,比如魔数、版本、类、常量、方法等等。所以我们这里只截取部分进行进行解析。
2. 解析魔数并校验
很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起到标识作用,叫作魔数(magic number)。
例如;
PDF 文件以 4 字节“%PDF”(0x25、0x50、0x44、0x46)开头,
ZIP 文件以 2 字节“PK”(0x50、0x4B)开头
class 文件以 4 字节“0xCAFEBABE”开头
读取字节码中的前四位,
-54, -2, -70, -66,将这四位转换为 16 进制。因为 java 中是没有无符号整型的,所以只能用更高位存放。
解析后就是魔数的对比,看是否与 CAFEBABE 一致。
测试结果
3. 解析版本号信息
刚才我们已经读取了 4 位魔数信息,接下来再读取 2 位,是版本信息。
魔数之后是 class 文件的次版本号和主版本号,都是 u2 类型。假设某 class 文件的主版本号是 M,次版本号是 m,那么完整的版本号可以表示成“M.m”的形式。次版本号只在 J2SE 1.2 之前用过,从 1.2 开始基本上就没有什么用了(都是 0)。主版本号在 J2SE 1.2 之前是 45,从 1.2 开始,每次有大版本的 Java 版本发布,都会加 1{45、46、47、48、49、50、51、52}
这里有一个小技巧,class 文件解析出来是一整片的内容,JVM 需要按照虚拟机规范,一段一段的解析出所有的信息。
同样这里我们需要把 2 位 byte 转换为 16 进制信息,并继续从第 6 位继续读取 2 位信息。组合出来的才是版本信息。
测试结果
4. 解析全部内容对照
按照 JVM 的加载过程,其实远不止魔数和版本号信息,还有很多其他内容,这里我们可以把测试结果展示出来,方便大家有一个学习结果的比对印象。
如果大家对这部分验证、准备、解析,的实现过程感兴趣,可以参照这部分用 Java 实现的 JVM 源码:https://github.com/fuzhengwei/itstack-demo-jvm
六、总结
学习 JVM 最大的问题是不好实践,所以本文以案例实操的方式,学习 JVM 的加载解析过程。也让更多的对 JVM 感兴趣的研发,能更好的接触到 JVM 并深入的学习。
有了以上这段代码,大家可以参照 JVM 虚拟机规范,在调试 Java 版本的 JVM,这样就可以非常容易理解整个 JVM 的加载过程,都做了什么。
如果大家需要文章中一些原图 xmind 或者源码,可以添加作者小傅哥(fustack),或者关注公众号:bugstack 虫洞栈进行获取。好了,本章节就扯到这,后续还有很多努力,持续原创,感谢大家的支持!
七、系列推荐
版权声明: 本文为 InfoQ 作者【小傅哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/d73ddc116dd1261646670e7bb】。文章转载请联系作者。











评论