虚拟机完成~

前言

首先甩仓库:wind jvm
一开始只是想写一个 java 的 class file parser。后来把这东西变成了一个 tool,请左转我的javap。后来看到 @racaljk 同学用 java 实现了一个小虚拟机,感觉很有意思,遂学习了一波规范,然后写了个 C++ 的。大三下准备实习,但是还没什么能拿出手的。所以正好现在大三上有点时间,于是花了两个月写了一个。

类似项目:

@racaljk 的 YVM。不过 @racaljk 学习貌似很忙,一直没有更新QAQ。最初知道你是因为你的 hosts,实在是帮了不少忙,非常感激。而且 programming 也很强,实在佩服。
@lfkdsk 学长的 JustVM。学长实在很强,看到一堆的个人项目就五体投地了。当时我也因为 Jar 文件解包比较麻烦,看到 @lfkdsk 是直接文件读取 zip 的。因为没有现成的 zip 库也不想在上边花太多时间,所以我就采取直接把 rt.jar 全部解压的省事策略了。话说回来,大工和我们还是邻居(,而且 @lfkdsk 学长貌似也是哈尔滨人OWO。
以及 @zxh0 大大的 jvm.go。虽然没学过 go 语言,不过还是能看懂一些,也参考了部分思路。
不过最重点的还是 openjdk 的 hotspot…… 虽然管中窥豹,不过也可以略见一斑了,学到了非常多的东西~

具体

我的 wind jvm 也就是个小玩具。代码总量用 cloc 去一下水分,也就 15k 左右。一共花了两个月代码时间,其实还有一个月是在学习各种乱七八糟的支持项目的知识。还 reference 了各种东西,列举如下:

  • jvm8 spec
  • 周志明大大的《深入理解 java 虚拟机》
  • 陈涛大大的《HotSpot 实战》
  • (日)中村成洋先生的《垃圾回收的算法与实现》(中译)
  • 等等。重要的还有一堆网络资源。比如 R大的 hllvm 论坛:hllvm论坛~

那么说下打开方式

我的 README 上都有写,在这里重写一遍。

  1. 首先我只支持 linux 和 mac。因为底层用了各种操作系统函数,pthread,stat 啥的。我的机器是 mac,所以就不支持 Windows 了。然后呢,我们需要 boost 库。用 brew 安装和 apt-get 啥的,yum 啥的都行。mac 就是 brew install boost,然后 ubuntu 应该是 sudo apt-get install libboost-all-dev
  2. 这样我们就有了 boost 支持了。于是我们应该去 Makefile 修改一下,因为我配置的是我机器的环境,而且没用 cmake。所以要手动修改,把我机器上的 boost 路径目录换成你的就可以了。比如如果是 mac 的话,把 ifeq 中的 $(CC) $(LINK_FLAGS) -L/usr/local/Cellar/boost/1.60.0_2/lib/ .... 里边的目录换成你自己的就行。如果是 linux,就把 else 中的 $(CC) $(LINK_FLAGS) -L/usr/lib/x86_64-linux-gnu/ 换成你自己的。不过如果是 ubuntu,八成不需要改,因为目录的版本无关。其他的 linux 就不知道了。
  3. 然后呢,你需要知道你的 jdk class 文件路径。mac 上,一般在 /Library/Java/JavaVirtualMachines/jdk1.8xxx.jdk/Contents/Home/jre/lib/ 下的 rt.jar 文件。如果是 linux,一般在 /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/ 下。配置到 config.xml 中相应位置就可以了。
  4. 于是应该就完事了。直接跑 make -j 8 啥的 8 线程编译就可以。当然如果你是虚拟机,虚拟内存没配置够的话就算了,直接跑 make -j 2 或者 make -j 3 这种就行了。
  5. 之后 bin 目录会出现 wind_jvm 这个 executable file。注意:一定要在 wind_jvm/ 目录运行 ./bin/wind_jvm Test1 这样的命令。因为内部我的 system lib path 是通过当前路径来获取的。如果不在 wind_jvm/ 目录下跑,就应该会报错。然后我给了十几个 TestX.java 文件,执行 make test 就能编译。有一个 Test7.java 是不行的。那个只有 debug version jvm 的工具才能编译。所以我编译好了直接放上去了。然后运行 ./bin/wind_jvm Test1 这种命令就好。不加 .class 后缀,参数必须有且仅有一个。
  6. 然后就可以玩了。不过只支持特定实现好的库,你要 socket 什么的都是没有的。不过日后实现看情况可以往上加,你也可以来 pull request 哦。
  7. 如果有 issue 请在 github 上上传 issue。

特性支持

  1. 完整的 ClassFileParser。那个 tool 的地址我已经刚才写过了。这东西才 3k 行多一点而已。
  2. 支持大部分常用反射机制。其他的是我没写的。因为太多了……不过想写的话肯定是有的。具体在 src/native/sun_reflect_Reflection.hpp(cpp)src/native/sun_reflect_NativeConstructorAccessorImpl.hpp(cpp) 等等文件中。
  3. 支持底层的 Unsafe 类中的大部分。如果想要支持 jdk 类库,这个类是必须要写且必须实现的。这个类可以添加 java 不能而 C++ 能的指针操作。必须强行交换两个对象什么的。当然,还需要有少量 CAS support。并发非常必要。
  4. 支持简单多线程。Thread 类的底层方法是通过操作系统级别的线程支持的。比如 pthread 库。
  5. 支持异常机制。stack unwind 栈回溯,athrow 以及可以 catch 字节码已经处理好的异常表。Test7.java 就是测试多线程异常的。
  6. 支持 GC。parallel GC 支持的保证是 stop-the-world (调 bug 可是好长好长时间好痛苦哇),使用了 GC-Root 算法,以及 GC 复制算法。见:gc.cpp……虽然代码量不大确是调试时间最长最难受的部分……毕竟这不是单线程 GC,是多线程的……不过我的实现肯定也是 too young 的,因为并没有各种菊苣的 paper 的支持。
  7. 部分支持 lambda,比如简单的 invokedynamic 类似 Thread t = new Thread(() -> System.out.println("hello world"));,Test4,5,6,8,11,13 是测试 lambda 和 invoke(MethodHandle) 的。不过很遗憾这一部分理解有些跟不上,虽然支持是比较容易,但是想要理解类库究竟是怎么实现 lambda,还需要积累和进一步研究。因此只能支持部分。[注:部分测试用例 from network]。具体实现代码请见:invokedynamic。当然不可能只有这点。这里是核心部分。还有待支持更多。
  8. 字节码方面,支持绝大多数,没用到的就没写(怕写错),wide 指令这种,我是没加的。

实现细节

  1. 和 openjdk 一样的,klass oop 二分模型。
  2. 解释型。完全在解释 bytecode。因此效率上肯定差强人意。
  3. 按照真正的 jvm 跑 class 文件的流程运行(几乎)。初始化 mirror,初始化基础类,使用 C++ 实现的 bootstraploader,进而调用 java 的 AppClassLoader 来加载 main class。
  4. 每个线程配上了不同的颜色(,方便观看(其实更重要的是方便调试哇。(笑)
  5. 等等。细节太多了。如果你想看更多的细节,下文有字节码执行的流程输出。

糟粕

  1. 一开始什么也不会的时候,用字符串查找类……这是特别悲伤的设计。严重拖慢速度,历史遗留问题。
  2. 有时跑 Test7 这种多线程测试用例碰到异常的时候,最后会段错误。其实是完全可以解决的。用 pthread_cancel 配上 pthread_join 就可以完全解决。不过一开始的设计是没考虑到这么多,直接把所有线程 detach 了。如果要改,势必代码的形状会特别悲伤。所以左思右想还是维持原状,并不影响运行结果。其实这样是并不对的,java 要求某一线程抛异常,不会影响其他线程的执行。其实真正的实现是要用 join 的。
  3. 等等。

输出细节

如果想要开启更多的细节,可以在 Makefile 中修改,原本是 CPP_FLAGS := -std=c++14 -O3 -pg,我在后边写了一个加上 -DBYTECODE_DEBUG -DDEBUG 的,启用这个就可以启用所有的字节码调试代码。引用一张月初放在博客上的图:
IMAGE
如果还要看 classfile 的文件 parse,运行时常量池的解析,以及字符串池的常量字符串,可以打开 -DKLASS_DEBUG -DPOOL_DEBUG -DSTRING_DEBUG 宏,然后重新编译就好。
不过!如果这么打开,由于一开始初始化虚拟机,需要加载各种类,需要跑各种字节码,输出会是巨量的。没记错的话跑一个 hello world 貌似就要上十w的字节码执行量吧。因为我是解释型,自然执行的机器码比这还多;如果是编译型,那就快了。所以如果发现终端吃不消,请及时关闭。后期由于字节码输出量巨大,我从来都是关闭这些宏的,只看结果忽略执行过程。

写文章的目的

为了骗 star(,同时也是为了交友(。希望有同样爱好和兴趣的童鞋能够一起偷(yu)税(yue)地交流。那么就写到这吧。同时发到博客和知乎。

最终的感谢

感谢这个领域的先驱们,感谢各种热心的回复,感谢给我思路和灵感的人们。(热泪盈眶)
尤为感谢《spec》《深入java虚拟机》《hotspot实战》的作者们,以及R大的论坛和R大在知乎上传播的各种深入的虚拟机知识。实在是感激不尽!
另:
就是一个区区玩具,就不胡乱@了。供学习和交流~随便乱加了一个开源协议,虽然没什么用处就是了。