首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Rust案例学习,从零开始写一个JVM

现在最明星的编程语言无疑是Rust,各种用Rust重写的项目层出不穷。Rust从零编写一个浏览器/游戏引擎/数据库的也很多。最近有一个小哥就用Rust写了一个Java虚拟机rjvm,今天我们就来学习一下。

概述

当然这个项目是作为一个好玩和学习的项目,并不能支持完整意义上所谓JVM应该具备的完整的功能。 主要实现了:

控制流语句( if, for, ...);

原始和对象创建;

虚方法和静态方法调用 ;

例外情;

垃圾收集GC;

解析一个jar包文件,比如:

它使用OpenJDK 7包含的t.jar类包。因此,在上面的示例中, java.lang.StackTraceElement类来自真正的JDK。

实现

代码组织

该代码是一个标准的Rust项目。项目可分为三个Rust板条箱(即包):

reader:它能够读取 .class文件并包含对其内容进行建模的各种类型;

vm:其中包含可以将代码作为库执行的虚拟机;

vm_cli:是一个非常简单的命令行启动器来运行虚拟机,可以java执行的。

后续考虑将reader板条箱提取到一个单独的库中并将其发布到crates.io上,可能有实际应用价值。

解析.class文件

众所周知,Java是一种编译语言-javac编译器接受.java源文件并产生种.class文件,然后打包到.jar文件(这只是一个zip压缩包)。因此,执行一些Java代码要做的第一件事就是加载一个.class文件,包含编译器生成的字节码。类文件包含各种内容:

有关类的元数据,例如其名称或源文件名

超类名称

实现的接口

字段及其类型和注释以及方法:

它们的描述符,它是一个字符串,表示每个参数的类型和方法的返回类型

元数据,例如 throws子句、注释、泛型信息

和字节码,以及一些额外的元数据,例如异常处理程序表和行号表。

为此,rjvm创建了一个板条箱,名为reader,它可以解析类文件并返回一个Rust 结构对类及其所有内容进行建模的。

执行方法

主要API vm板条箱是Vm::invoke,用于执行方法。它需要一个CallStack,其中将包含各种 CallFrame,每个正在执行的方法一个。用于执行main,调用堆栈最初将为空,并且将创建一个新框架来运行它。然后,每次函数调用都会向调用堆栈添加一个新帧。当方法执行完成时,其相应的帧将被丢弃并从调用堆栈中删除。

大多数方法将用Java实现,因此它们的字节码将被执行。然而, rjvm还支持本机方法,即直接由JVM实现而不是在Java字节码中实现的方法。其中相当多的部分位于Java API 的“较低部分”,需要与操作系统交互(例如,执行 I/O)或支持运行时。可能见过的后者的一些示例包括 System::currentTimeMillis, System::arraycopy, 或者 Throwable::fillInStackTrace。 在 rjvm,这些都由Rust 函数实现的。

JVM是基于堆栈的虚拟机,即字节码指令主要在值堆栈上操作。还有一组由索引标识的局部变量,可用于存储值并将参数传递给方法。这些与每个调用帧相关联 rjvm。

值和OO建模

类型Value对局部变量、堆栈元素或对象字段的可能值进行建模,并按如下方式实现:

为了存储对象及其值,最初使用了一个名为的简单结构Object包含对类的引用(以建模对象的类型)和Vec用于存储字段的值。然而,当实现GC时,修改了它以使用较低级别的实现,带有大量的指针和强制转换,相当C风格。在最新的实现中,AbstractObject(它模拟“真实”对象或数组)只是一个指向字节数组的指针,其中包含几个标头字,然后是字段值。

执行指令

执行一个方法意味着一次执行一个字节码指令。JVM 拥有大量指令(超过200 条),由字节码中的一个字节进行编码。许多指令后面都有参数,有些指令的长度是可变的。这是在代码中按类型建模的Instruction:

如上所述,方法的执行将保留一个堆栈和一个局部变量数组,指令通过其索引引用它们。它还会将程序计数器初始化为零,即下一条要执行的指令的地址。该指令将被处理并更新程序计数器,通常前进1,但各种跳转指令可以将其移动到不同的位置。这些用于实现所有流程控制语句,例如if,for和while。

一个特殊的指令系列由那些可以调用另一种方法的指令组成。有多种方法可以解决应该调用哪个方法:虚拟或静态查找是主要方法,但还有其他方法。解决正确的指令后,rjvm将向调用堆栈添加一个新帧并开始该方法的执行。方法的返回值将被压入堆栈,除非void,并且执行将恢复。

异常处理

异常的实现是相当复杂的,因为它们破坏了正常的控制流,并且可能从方法中提前返回(并在调用堆栈上传播)。

需要知道的第一件事是任何catchblock对应于方法的异常表的一个条目,每个条目包含覆盖的程序计数器的范围、catch 块中第一条指令的地址以及该块捕获的异常的类名称。

接下来是签名CallFrame::execute_instruction如下:

其中类型是:

和标 Rust Result类型则是:

因此,执行一条指令可能会导致四种可能的状态:

指令执行成功,当前方法可以继续执行(标准情况);

该指令执行成功,并且它是一个返回指令,因此当前方法应该返回(可选)一个返回值;

该指令无法执行,因为发生了一些内部VM错误;

或者指令无法执行,因为抛出了标准Java异常。

执行一个方法的代码如下:

这段代码中有相当多的实现细节,可以由此学习如何使用Rust Resul模式匹配映射了上述行为的描述。

垃圾收集

最后的里程碑rjvm已经实现垃圾收集器GC。选择的算法是stop-the-world(这简单,因为没有线程)半空间复制收集器。还实现了的一个切尼算法(较差的)变体。

这个想法是将可用内存分成两部分,称为半空间:一部分将处于活动状态并用于分配对象,另一部分将不使用。当空间满了时,将触发垃圾收集,所有活动对象将被复制到另一个半空间。然后,所有对对象的引用都将被更新,以便它们指向新的副本。最后,两者的角色将互换——类似于蓝绿部署的工作原理,原理草图如下:

该算法具有以下特点:

显然,它浪费了大量内存(可能的最大内存的一半!);

分配速度非常快(碰撞指针);

复制和压缩对象意味着它不必处理内存碎片;

由于更好的缓存行利用率,压缩对象可以提高性能。

真正的Java VM使用更复杂的算法,通常是分代垃圾收集器 ,例如G1或并行GC,它们使用复制策略的演变。

结论

通过rjvm,可以学习很多东西,不光可以提升Rust编程功力,也可以了解Java&JVM一些基本原理,最重要的是过程还很好玩。项目所有代码均已开源可以在Github学习(github /andreabergia/rjvm)或者通过gitee镜像(gitee /ijz/rjvm)。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OIc8tx1F5PukfZhRmMyo0fKw0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券