目录

ToyJVM 学习

前段时间看了下 Elk(十分简单的 JS VM,其实算是个 parser + interpreter 吧)的代码,学到了一些计算机基础的东西。

比如,我写代码这么久,甚至不知道 double 的内存结构和 7FF 代表什么。比如,因为没有系统学习过编译原理等等,没有 opcode 相关的概念,如何设计 interpreter(又想回去读书了···)。

当然了,我的本职是一名 Android 开发,Java/Kotlin 才是主语言,且 ART 和 openJDK 源码是开源的,都是学习的宝库,所以我也偶尔研究一下工作中遇到的相关代码。

不过,看过 ART 和 JVM 相关源码的人都知道,这部分代码经历多年发展和各种高端优化,除非你是专业领域人员,否则一时半会难以读懂。

所幸,总有爱学习爱研究的大佬们,为我们抽丝剥茧,找到精髓,ToyJVM 我觉得就是一个很好的学习教程(总共1~2千行代码)

Class 的概念

既然 ToyJVM 可以运行简单的标准的 class 文件,那么一定有 class 文件格式的解析,通过这部分代码,更加了解 class 文件格式

JVM 文档

ToyJVM 代码(多了个 initalized,似乎也没用):

typedef struct ClassFile{
    uint32_t       magic;
    uint16_t       minor_version;
    uint16_t       major_version;
    uint16_t       constant_pool_count;
    cp_info*       constant_pool; //[constant_pool_count-1]
    uint16_t       access_flags;
    uint16_t       this_class;
    uint16_t       super_class;
    uint16_t       interfaces_count;
    uint16_t*      interfaces;
    uint16_t       fields_count;
    field_info*    fields;
    uint16_t       methods_count;
    method_info*   methods;
    uint16_t       attributes_count;
    attribute_info* attributes;
    uint8_t initalized;
}ClassFile;

当然,真实代码肯定不是这么简单了,真的加载一个 class, 又需要 ClassLoader,这又是一个十分复杂的过程。

因为目的只是简单的执行相关的代码,也不考虑 jni 等相关调用。

核心只有几个问题

  1. 如何解析 class
    这里使用 fread 按照 class 格式读取。不过动态大小的直接使用了指针。感觉也可以直接使用 mmap。
    constant_pool = cp_info * constant_pool_count
    其余类似,均是 struct * count 结构排列
    每个 struct 中有各自的内存结构,详见 JVM 文档

  2. 如何找到一个 method
    class 中有 method_info 结构,通过 method 的 name_index 可以去 constant pool 中查找(class 是一个紧凑结构,所有的字符串都是存在一起的,如果有相同字符串其实是公用的) method 名称,然后通过 descriptor 确定唯一 method_info(因为可能有重载)

  3. method 中的 code 如何运行
    这就是 opcode 和 stack frames(栈帧) 的概念了

栈帧的概念

像很多时候,我们面试的时候可能会被问到相关的“八股问题”,网络上也有各种文章来讲解什么是栈帧,但是往往都是一些理论模型,模糊不清

ToyJVM 中代码实现了栈帧的概念,且和理论模型十分接近。

JVM 文档 介绍,Frames 中存在几个概念:

  1. Local Variables 本地变量
  2. Operand Stacks 操作栈
  3. Dynamic Linking 动态链接
  4. Others

ToyJVM 中的代码

typedef struct Frame{
    LocalVariable* localVariables;
    struct Stack* operandStack;
    struct Machine* machine;
    struct ClassFile* classRef;
    uint32_t pc; // program counter
    Code_attribute* code;
    //A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file (§4.4).
    //It contains several kinds of constants, ranging from numeric literals known at compile-time to method and field references that must be resolved at run-time.
} Frame;

因为还要知道当前的指令执行到哪里了,所以多了一个 pc 计数器

首先,明确一个概念,一个 method 在 class 中体现为多条指令,存储在 Code_attribute 的 code[] 中

那么虚拟机如何运行方法的 code[]? 如何传参?获取返回值?

这就是栈帧的作用了

以 toyJVM 举例

找到 Main 方法以后,找到其 code[],创建 stack frame,压入 JVM stack,然后通过 pc 遍历执行所有指令,每一个指令(opcode) 又有各自的处理方式,又是一块对应的知识。根据各自 opcode 执行对应指令,或者,寻找对应的照约定所存在位置的值。这些操作都会在 operandStack 中完成。

然后执行到 return 的 opcode 后,如果还有其他指令(不考虑 try-catch 之类的),说明还有其他方法调用。此时,还要将当前的 operanStack 中的值,或者是调用方法所需要的 local variables 值等 copy 到一个新的 stack frame,然后 destroy 当前 frame,new stack frame 然后压栈。

如此往复。

当然,无论 stack frame,还是stack frame 中 operandStack 本身的大小有限制,如果超过了指定大小,也会有对应的栈溢出异常

Heap 的概念

ToyJVM 中只有一个十分简单的 heap,并没有任何 gc 相关的逻辑,不然整体的代码量肯定又会大涨

但是,如同 Elk 的 heap 一样,我们脑海中已经有了一个实体的概念。

VM 有自己的内存分配器,有自己的 heap,然后我们可以基于自己的内存分配逻辑,来记录整体 VM 的内存占用,如果辅以合理的 gc 算法,其实也就成了一个最基本的 VM 的内存模块。

当然,这里有各种各样的细节问题。比如:

  1. JVM 的并发 gc,如何保证 gc 发生的同时,其他线程不会继续使用该对象?
  2. 我们面试的八股文中的 copy 算法,两片内存空间,用于 gc 时对象的复制和内存碎片整理,那么随着对象的移动,又怎么保证我们的对象引用一直是正确的?

总结

截止目前,我对于 VM 的学习,甚至不是冰山中的一角,只是冰山中的一块冰。但是,对于这方面的基础知识,越来越扎实。

希望某一天我可以自己写一个完整的 JS Engine。