jvm规范中文版_大牛程序员用Java手写JVM:刚好够运行 HelloWorld

专注于Java领域优质技术号,欢迎关注

作者:老曹撸代码

776dd903408d0bdf78aede6942d792cd.gif

1. 前言

没错这又是一篇介绍 JVM 的文章,这类文章网上已经很多,不同角度、不同深度、不同广度,也都不乏优秀的。为什么还要来一篇?首先对于我来说,我正在学习 Java,了解JVM的实现对学习Java当然很有必要,但我已经做了多年C++开发,就算我用C++实现一个JVM,我还是个C++码农,而用 Java实现,即能学习 Java 语法,又能理解 JVM,一举两得。其次,作为读者,hotspot或者其他成熟JVM实现的源码读起来并不轻松,特别是对没有C/C++经验的人来说,如果只是想快速了解JVM的工作原理,并且希望运行和调试一下JVM的代码来加深理解,那么这篇文章可能更合适。

我将用Java实现一个JAVA虚拟机(源码在这下载:https://github.com/caoym/jjvm,加 Star 亦可),一开始它会非常简单,实际上简单得只够运行HelloWorld。虽然简单,但是我尽量让其符合 JVM 标准,目前主要参考依据是《Java虚拟机规范 (Java SE 7 中文版)》。

2. 准备

先写一个HelloWorld,代码如下:

2ebb75b0651015f71413732e046b4641.png

我期望所实现的虚拟机(姑且命名为JJvm吧),可以通过以下命令运行:

89e5a61a55eaf1a72b4dee8525e6aced.png

接下来我们开始实现JJvm,下面是其入口代码,后面将逐步介绍:

580090dd4403d93e17933dc846d4110b.png

3. 加载初始类

我们将包含 main 入口的类称为初始类,JJvm 首先需要根据org.caoym.HelloWorld类名,找到.class 文件,然后加载并解析、校验字节码,这些步骤正是 ClassLoader(类加载器)做的事情。HelloWorld.class内容大致如下:

fe44dae8c90880d1d02de2de010264e7.png

没错是紧凑的二进制格式,需要按规范解析,不过我并不打算自己写解析程序,可以直接用com.sun.tools.classfile.ClassFile,这也是用JAVA写好处。下面是HelloWorld.class解析后的内容(通过javap -v HelloWorld.class输出):

d07833fe0efb0b70fc5fdb6bced7bbfa.png
6815c4a8ee1f48dc1cacc2fbf83061f8.png
c9e36fb63dfabc3ca950d8f68b8e6e94.png

可以看到HelloWorld.class 文件中主要包含几部分:

  1. 常量池(Constant pool)
  2. 常量池中记录了当前类中用到的常量,包括方法名、类名、字符串常量等,如:#3 = String #23, #3为此常量的索引,字节码执行时通过此索引获取此常量,String为常量类型, 还可以是Methodref (方法引用)、Fieldref(属性引用)等。
  3. 方法定义
  4. 此处定义了方法的访问方式(如 PUBLIC、STATIC)、字节码等,关于字节码的执行方式将在后面介绍。

以下为类加载器的部分代码实现:

3528474428c32571dc0b6a2ae500beb4.png

类加载器可以加载两种形式的类:JvmOpcodeClass和 JvmNativeClass,均继承自JvmClass。其中JvmOpcodeClass 表示用户定义的类,通过字节码执行,也就是这个例子中的HelloWorld;JvmNativeClass表示JVM 提供的原生类,可直接调用原生类执行,比如 java.lang.System。这里把所有非项目内的类,都当做原始类处理,以便简化虚拟机的实现。

4. 找到入口方法

JVM规定入口是static public void main(String[]),为了能够查找指定类的方法,JvmOpcodeClass和JvmNativeClass都需要提供getMethod方法, 当然 main 方法肯定存在JvmOpcodeClass中:

e6904abd9d8890cb83c11db668d8ba2d.png

5. 执行非 Native(字节码定义的)方法

下图为以HelloWorld的main()方法的执行过程:

89a69053b228a5f8b53ac7facd731077.gif

下面将详细说明。

5.1. 虚拟机栈

每一个虚拟机线程都有自己私有的虚拟机栈(Java Virtual Machine Stack),用于存储栈帧。每一次方法调用,即产生一个新的栈帧,并推入栈顶,函数返回后,此栈帧从栈顶推出。以下为 JJvm中虚拟机栈的部分代码:

b6193225b8cb9da2388ebe9aef440b96.png

5.2. 栈帧

栈帧用于保存当前函数调用的上下文信息,以下为 JJvm 中栈帧的部分代码:

bb12934ea8e4b21737c5ca2bf3471e7a.png

说明:

  • 局部变量表
  • 保存当前方法的局部变量、实例的this指针和方法的实参。函数执行过程中,部分字节码会操作或读取局部变量表。局部变量表的长度由编译期决定。
  • 常量池
  • 引用当前类的常量池。
  • 字节码内容
  • 以数组形式保存的当期方法的字节码。
  • 程序计数器
  • 记录当前真在执行的字节码的位置。
  • 操作数栈
  • 操作数栈用来准备字节码调用时的参数并接收其返回结果,操作数栈的长度由编译期决定。

5.3. 方法调用

方法调用的过程大致如下:

  1. 新建栈帧,并推入虚拟机栈。
  2. 将实例的this和当前方法的实参设置到栈帧的局部变量表中。
  3. 解释执行方法的字节码。

以下为 JJvm 中的部分代码:

3b8222c5ecbf663d77684991e411d0b0.png
7e0f3b2eed33b86e42c9b865a62137ad.png

5.4. 解释执行字节码

字节码的执行过程如下:

  1. 获取栈顶的第一个栈帧。
  2. 获取当前栈的程序计数器(PC,其默认值为0)指向的字节码,程序计数器+1。
  3. 执行上一步获取的字节码,推出操作数栈的元素,作为其参数,执行字节码。
  4. 字节码返回的值(如果有),重新推入操作数栈。
  5. 如果操作数为return等,则设置栈帧为已返回状态。
  6. 如果操作数为invokevirtual等嵌套调用其他方法,则创建新的栈帧,并回到第一步。
  7. 如果栈帧已设置为返回,则将返回值推入上一个栈帧的操作数栈,并推出当前栈。
  8. 重复执行1~7,直到虚拟机栈为空。

以下为JJvm中解释执行字节码的部分代码:

833ee2156d609b843ca10748b735485e.png
80d24d42796a57c5bff118ddfbf1f5bc.png
eccb4628084a8df244661529c070991f.png
cf56583b6ffdd7719970338b3c50b66a.png

6. 执行 Native 方法

Native方法的调用要更简单一些,只需调用已存在的实现即可,代码如下:

62488bc2257ffc536c370ab27b8585d3.png

7. 结束

到目前为止,我们的“刚好够运行 HelloWorld”的 JVM 已经完成,完整代码可在这里下载:https://github.com/caoym/jjvm。当然这个JVM 并不完整,缺少很多内容,如类和实例的初始化、多线程问题、反射、GC 等等。我争取逐步完善JJvm,并奉上更多文章。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值