用 C++ 实现了一个小型的 jvm
一开始只是想写一个 java 的 class file parser。后来把这东西变成了一个 tool,请左转看我的小工具 javap。后来看到 @racaljk
同学用 java 实现了一个小虚拟机,感觉很有意思,遂学习了一波规范,然后写了个 C++ 的。
// 注:原先编辑的文章中有类似项目小标题。不过想想还是不太好,因此去掉了。但是依然对其他几位有兴趣的同好的代码思路带来的启迪表示感激。 具体我的 wind jvm 也就是个小玩具。代码总量用 cloc 去一下水分,也就 15k 左右。一共花了两个月代码时间,其实还有一个月是在学习各种乱七八糟的支持项目的知识。还 reference 了各种东西,列举如下: - jvm8 spec
- 周志明大大的《深入理解 java 虚拟机》
- 陈涛大大的《HotSpot 实战》
- (日)中村成洋先生的《垃圾回收的算法与实现》(中译)
- 等等。重要的还有一堆网络资源。比如 R大的 hllvm 论坛:hllvm论坛~
那么说下打开方式我的 README 上都有写,在这里重写一遍。 - 首先我只支持 linux 和 mac。因为底层用了各种操作系统函数,pthread,stat 啥的。我的机器是 mac,所以就不支持 Windows 了。然后呢,我们需要 boost 库。用 brew 安装和 apt-get 啥的,yum 啥的都行。mac 就是 [size=0.9em]brew install boost,然后 ubuntu 应该是 [size=0.9em]sudo apt-get install libboost-all-dev。
- 这样我们就有了 boost 支持了。于是我们应该去 Makefile 修改一下,因为我配置的是我机器的环境,而且没用 cmake。所以要手动修改,把我机器上的 boost 路径目录换成你的就可以了。比如如果是 mac 的话,把 ifeq 中的 [size=0.9em]$(CC) $(LINK_FLAGS) -L/usr/local/Cellar/boost/1.60.0_2/lib/ .... 里边的目录换成你自己的就行。如果是 linux,就把 else 中的 [size=0.9em]$(CC) $(LINK_FLAGS) -L/usr/lib/x86_64-linux-gnu/ 换成你自己的。不过如果是 ubuntu,八成不需要改,因为目录的版本无关。其他的 linux 就不知道了。
- 然后呢,你需要知道你的 jdk class 文件路径。mac 上,一般在 [size=0.9em]/Library/Java/JavaVirtualMachines/jdk1.8xxx.jdk/Contents/Home/jre/lib/ 下的 rt.jar 文件。如果是 linux,一般在 [size=0.9em]/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/ 下。配置到 config.xml 中相应位置就可以了。
- 于是应该就完事了。直接跑 make -j 8 啥的 8 线程编译就可以。当然如果你是虚拟机,虚拟内存没配置够的话就算了,直接跑 make -j 2 或者 make -j 3 这种就行了。
- 之后 [size=0.9em]bin 目录会出现 [size=0.9em]wind_jvm 这个 executable file。注意:一定要在 [size=0.9em]wind_jvm/ 目录运行 [size=0.9em]./bin/wind_jvm Test1 这样的命令。因为内部我的 system lib path 是通过当前路径来获取的。如果不在 [size=0.9em]wind_jvm/ 目录下跑,就应该会报错。然后我给了十几个 TestX.java 文件,执行 [size=0.9em]make test 就能编译。有一个 Test7.java 是不行的。那个只有 debug version jvm 的工具才能编译。所以我编译好了直接放上去了。然后运行 [size=0.9em]./bin/wind_jvm Test1 这种命令就好。不加 [size=0.9em].class 后缀,参数必须有且仅有一个。
- 然后就可以玩了。不过只支持特定实现好的库,你要 socket 什么的都是没有的。不过日后实现看情况可以往上加,你也可以来 pull request 哦。
- 如果有 issue 请在 github 上上传 issue。
特性支持- 完整的 ClassFileParser。那个 tool 的地址我已经刚才写过了。这东西才 3k 行多一点而已。
- 支持大部分常用反射机制。其他的是我没写的。因为太多了......不过想写的话肯定是有的。具体在 [size=0.9em]src/native/sun_reflect_Reflection.hpp(cpp),[size=0.9em]src/native/sun_reflect_NativeConstructorAccessorImpl.hpp(cpp) 等等文件中。
- 支持底层的 Unsafe 类中的大部分。如果想要支持 jdk 类库,这个类是必须要写且必须实现的。这个类可以添加 java 不能而 C++ 能的指针操作。必须强行交换两个对象什么的。当然,还需要有少量 CAS support。并发非常必要。
- 支持简单多线程。Thread 类的底层方法是通过操作系统级别的线程支持的。比如 pthread 库。
- 支持异常机制。stack unwind 栈回溯,athrow 以及可以 catch 字节码已经处理好的异常表。Test7.java 就是测试多线程异常的。
- 支持 GC。parallel GC 支持的保证是 stop-the-world (调 bug 可是好长好长时间好痛苦哇),使用了 GC-Root 算法,以及 GC 复制算法。见:gc.cpp......虽然代码量不大确是调试时间最长最难受的部分......毕竟这不是单线程 GC,是多线程的......不过我的实现肯定也是 too young 的,因为并没有各种菊苣的 paper 的支持。
- 部分支持 lambda,比如简单的 invokedynamic 类似 [size=0.9em]Thread t = new Thread(() -> System.out.println("hello world"));,Test4,5,6,8,11,13 是测试 lambda 和 invoke(MethodHandle) 的。不过很遗憾这一部分理解有些跟不上,虽然支持是比较容易,但是想要理解类库究竟是怎么实现 lambda,还需要积累和进一步研究。因此只能支持部分。[注:部分测试用例 from network]。具体实现代码请见:invokedynamic。当然不可能只有这点。这里是核心部分。还有待支持更多。
- 字节码方面,支持绝大多数,没用到的就没写(怕写错),wide 指令这种,我是没加的。
实现细节- 和 openjdk 一样的,klass oop 二分模型。
- 解释型。完全在解释 bytecode。因此效率上肯定差强人意。
- 按照真正的 jvm 跑 class 文件的流程运行(几乎)。初始化 mirror,初始化基础类,使用 C++ 实现的 bootstraploader,进而调用 java 的 AppClassLoader 来加载 main class。
- 每个线程配上了不同的颜色(,方便观看(其实更重要的是方便调试哇。(笑)
- 等等。细节太多了。如果你想看更多的细节,下文有字节码执行的流程输出。
糟粕- 一开始什么也不会的时候,用字符串查找类......这是特别悲伤的设计。严重拖慢速度,历史遗留问题。
- 有时跑 Test7 这种多线程测试用例碰到异常的时候,最后会段错误。其实是完全可以解决的。用 pthread_cancel 配上 pthread_join 就可以完全解决。不过一开始的设计是没考虑到这么多,直接把所有线程 detach 了。如果要改,势必代码的形状会特别悲伤。所以左思右想还是维持原状,并不影响运行结果。其实这样是并不对的,java 要求某一线程抛异常,不会影响其他线程的执行。其实真正的实现是要用 join 的。
- 等等。
输出细节如果想要开启更多的细节,可以在 Makefile 中修改,原本是 CPP_FLAGS := -std=c++14 -O3 -pg,我在后边写了一个加上 -DBYTECODE_DEBUG -DDEBUG 的,启用这个就可以启用所有的字节码调试代码。引用一张月初放在博客上的图: 题图就是开启宏之后的输出结果。
如果还要看 classfile 的文件 parse,运行时常量池的解析,以及字符串池的常量字符串,可以打开 -DKLASS_DEBUG -DPOOL_DEBUG -DSTRING_DEBUG 宏,然后重新编译就好。
不过!如果这么打开,由于一开始初始化虚拟机,需要加载各种类,需要跑各种字节码,输出会是巨量的。没记错的话跑一个 hello world 貌似就要上十w的字节码执行量吧。因为我是解释型,自然执行的机器码比这还多;如果是编译型,那就快了。所以如果发现终端吃不消,请及时关闭。后期由于字节码输出量巨大,我从来都是关闭这些宏的,只看结果忽略执行过程。
用 C++ 实现了一个小型的 jvm
游客,本帖隐藏的内容需要积分高于 2 才可浏览,您当前积分为 0
提取码下载:
|