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

我用 Rust 编写了一个JVM

这篇文章是作者分享他如何用 Rust 编写一个 Java 虚拟机(JVM)的经验。他强调这是一个玩具级别的 JVM,主要用于学习目的,并非严肃的实现。尽管如此,他实现了一些非琐碎的功能,如控制流语句、对象创建、方法调用、异常处理、垃圾收集等。他还详细介绍了代码组织、文件解析、方法执行、值和对象的建模、指令执行、异常处理和垃圾收集等方面的实现细节。

链接:https://andreabergia.com/blog/2023/07/i-have-written-a-jvm-in-rust/

未经允许,禁止转载!

作者 | Andrea Bergia       责编 | 明明如月

责编 | 夏萌

出品 | CSDN(ID:CSDNnews)

最近,我一直在学习 Rust,和任何理智的人一样,编写了几个百行的程序后,我决定做点更加有挑战的事情:我用 Rust 写了一个 Java 虚拟机(Java Virtual Machine)。我极具创新地将其命名为 rjvm。你可以在 GitHub 上找到源代码。

我想强调的是,这只是为了学习而构建的一个玩具级别的 JVM,而不是一个严肃的实现。

它不支持:

泛型

线程

反射

注解

I/O

即时编译器

字符串 intern 功能

然而,有一些非常琐碎的东西已经实现了:

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

基本类型和对象的创建

虚拟和静态方法的调用

异常处理

垃圾回收

从 jar文件解析类

以下是测试套件的一部分:

它使用的是真正的 rt.jar,里面包含了 OpenJDK 7 的类 —— 因此,在上面的例子中,java.lang.StackTraceElement 类就是来自真正的 JDK!

我对我所学到的东西感到非常满意,无论是关于 Rust 还是关于如何实现一个虚拟机。我对我实现的一个真正的、可运行的、垃圾回收器感到格外高兴。虽然它很一般,但它是我写的,我很喜欢它。既然我已经达成了我最初的目标,我决定在这里停下来。我知道有一些问题,但我没有计划去修复它们。

概述

在这篇文章中,我将给你介绍我的 JVM 是如何运行的。在接下来的文章中,我将更详细地讨论这里所涉及的一些方面。

代码组织

这是一个标准的 Rust 项目。我将其分成了三个包(也就是 crates):

reader,它能够读取 .class 文件,并包含了一些类型,用于模型化它们的内容;

vm,包含了一个可以作为库执行代码的虚拟机;

vm_cli,包含了一个非常简单的命令行启动器,用于运行 VM,这与 java 可执行文件的精神是一致的。

我正在考虑将 reader 包提取到一个单独的仓库中,并发布到 crates.io,因为它实际上可能对其他人有所帮助。

解析 .class 文件

众所周知,Java 是一种编译型语言 —— javac 编译器将你的 .java 源文件编译成各种 .class 文件,通常分布在 .jar 文件中,这只是一个 zip 文件。因此,执行一些 Java 代码的第一件事就是加载一个 .class 文件,其中包含了编译器生成的字节码。一个类文件包含了各种东西:

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

超类名称

实现的接口

字段,连同它们的类型和注解

方法和:

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

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

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

如上所述,对于 rjvm,我创建了一个单独的包,名为 reader,它可以解析一个类文件,并返回一个 Rust 结构,该结构模型化了一个类及其所有内容。

执行方法

vm 包的主要 API 是 Vm::invoke,用于执行方法。它需要一个 CallStack 参数,这个参数会包含多个 CallFrame,每一个 CallFrame 对应一种正在执行的方法。执行 main 方法时,调用栈将初始为空,会创建一个新的栈帧来运行它。然后,每一个函数调用都会在调用栈中添加一个新的栈帧。当一个方法的执行结束时,与其对应的栈帧将被丢弃并从调用栈中移除。

大多数方法会使用 Java 实现,因此将执行它们的字节码。然而,rjvm 也支持原生方法,即直接由 JVM 实现,而非在 Java 字节码中实现的方法。在 Java API 的“较底层”中有很多此类方法,这些部分需要与操作系统交互(例如进行 I/O)或需要运行时支持。你可能见过的后者的一些示例包括 System::currentTimeMillis、System::arraycopy 或 Throwable::fillInStackTrace。在 rjvm 中,这些都是通过 Rust 函数来实现的。

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

建模值和对象

Value 类型用于模拟局部变量、栈元素或对象字段可能的值,实现如下:

顺便提一句,这是 Rust 的枚举类型(求和类型)的一种绝妙抽象应用场景,它非常适合表达一个值可能是多种不同类型的事实。

对于存储对象及其值,我最初使用了一个简单的结构体 Object,它包含一个对类的引用(用来模拟对象的类型)和一个 Vec 用于存储字段值。然而,当我实现垃圾收集器时,我修改了这个结构,使用了更低级别的实现,其中包含了大量的指针和类型转换,相当于 C 语言的风格!在当前的实现中,一个 AbstractObject(模拟一个“真实”的对象或数组)仅仅是一个指向字节数组的指针,这个数组包含几个头部字节,然后是字段的值。

执行指令

执行方法意味着逐一执行其字节码指令。JVM 拥有一长串的指令(超过两百条!),在字节码中由一个字节编码。许多指令后面跟有参数,且一些具有可变长度。在代码中,这由类型 Instruction 来模拟:

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

另有一类特殊的指令是那些可以调用另一个方法的指令。解析应调用哪个方法有多种方式:虚拟或静态查找是主要方式,但还有其他方式。解析正确的指令后,rjvm 将向调用堆栈添加一个新帧,并启动方法的执行。除非方法的返回值为 void,否则将把返回值推到堆栈上,并恢复执行。

Java 字节码格式相当有趣,我打算专门发一篇文章来讨论各种类型的指令。

异常处理

异常处理是一项复杂的任务,因为它打破了正常的控制流,可能会提前从方法中返回(并在调用堆栈中传播!)。尽管如此,我对自己实现的方式感到相当满意,接下来我将展示一些相关的代码。

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

接着,CallFrame::execute_instruction 的签名如下:

其中的类型定义为:

标准的 Rust Result 类型是:

因此,执行一个指令可能会产生四种可能的状态:

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

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

无法执行指令,因为发生了某种内部 VM 错误;

无法执行指令,因为抛出了一个标准的 Java 异常。

因此,执行方法的代码如下:

我知道这段代码中包含了许多实现细节,但我希望它能展示出 Rust 的 Result 和模式匹配如何很好地映射到上述行为描述。我必须说我对这段代码感到相当自豪。

垃圾回收

在 rjvm 中,最后一个里程碑是实现垃圾回收器。我选择的算法是一个停止 - 世界(这显然是由于没有线程!)半空间复制收集器。我实现了 Cheney 的算法的一个较差的变体 - 但我真的应该去实现真正的 Cheney 算法。

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

这个算法有以下特点:

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

分配操作非常快(只需移动一个指针);

复制并压缩对象意味着无需处理内存碎片问题;

压缩对象可以提高性能,因为它更好地利用了缓存行。

实际的 Java 虚拟机使用了更为复杂的算法,通常是分代垃圾收集器,如 G1 或并行 GC,这些都使用了复制策略的进化版本。

结论

在编写 rjvm 的过程中,我学到了很多,也很有趣。从一个小项目中能学这么多,我已经很满足了。也许下次我在学习新的编程语言时会选择一个稍微不那么难的项目!

顺便说一句,使用 Rust 语言写代码给我带来了很好的编程体验。正如我之前写过的,我认为它是一种很棒的语言,我在用它来实现我的 JVM 时,确实享受到了它带来的各种乐趣!

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

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券