手写 JVM —— 3. 线程私有数据区
前言
本章实现线程私有的运行时数据区,为下一章实现字节码解释器做准备。
本节的代码位于 https://github.com/CN-GuoZiyang/SpicyChickenJVM/tree/954a17c9c7b83e917ddf028c5702d8c3b8c518f4
运行时数据区
在运行 Java 程序时,JVM 需要内存来存放各式各样的数据。JVM 规范将这些内存区域称为运行时数据区。运行时数据区可以分为两类:线程共享的的线程私有的。线程共享的部分在 JVM 启动时就会被创建,在 JVM 关闭时销毁;而线程私有的部分生命周期与线程一致,随着线程创建而创建,随着线程销毁而销毁。
线程共享的区域主要就是堆和方法区。堆用于存放对象实例,由垃圾收集器清理;方法区则存放字段和方法信息、类数据等。逻辑上来说,方法区也属于堆的一部分。
线程私有的区域包括 pc 寄存器和 Java 虚拟机栈。pc 寄存器用于存储当前执行的字节码行号,Java 虚拟机栈用于记录方法调用,由栈帧构成,栈帧中保存了方法运行状态,由局部变量表和操作数栈组成。
本章我们只实现线程私有的数据区,线程公有的部分在下下章实现。本章实现的类都保存在 rtda (runtime-data area)包中。
线程与虚拟机栈
我们首先来实现对一个线程结构的抽象:
public class Thread { // 程序计数器 private int pc; // 虚拟机栈 private JvmStack stack; public Thread() { this.stack = new JvmStack(1024); } }
当前只定义了 pc 和 stack 两个属性,分别代表 pc 寄存器和虚拟机栈。创建线程时同时会初始化一个可以容纳 1024 帧的虚拟机栈。
定义 pushFrame 和 popFrame 方法,用于向栈中压入或从栈中弹出帧,实际上只是包装 JvmStack 类的方法。currentFrame 返回当前正在执行的方法的栈帧。
public void pushFrame(Frame frame) { this.stack.push(frame); } public Frame popFrame() { return this.stack.pop(); } public Frame currentFrame() { return this.stack.top(); }
我们使用链表来实现 Java 虚拟机栈,这样从栈中弹出的帧就会自动被自动垃圾回收,同时这样栈可以按需使用空间。
public class JvmStack { private int maxSize; private int size; // 栈顶指针 private Frame _top; public JvmStack(int maxSize) { this.maxSize = maxSize; } }
构造方法中的 maxSize 定义了栈的最大容量。size 保存了当前栈的大小,_top 是栈顶指针,指向栈顶的第一帧。
接着实现 push、pop 和 top 方法,分别向栈中压入、从栈中弹出和返回栈顶的帧,这部分实现很简单,就是链表操作而已:
public void push(Frame frame) { if(this.size > this.maxSize) { throw new StackOverflowError(); } if(this._top != null) { frame.lower = this._top; } this._top = frame; this.size ++; } public Frame pop() { if(this._top == null) { throw new RuntimeException("Jvm stack is empty!"); } Frame top = this._top; this._top = top.lower; top.lower = null; this.size --; return top; } public Frame top() { if(this._top == null) { throw new RuntimeException("Jvm stack is empty!"); } return this._top; }
在栈已经满了的情况下,再 push 就会抛出 StackOverflowError 异常。
接着我们就可以着手实现帧结构了,帧是栈链表的基本元素,所以需要一个指针指向它下面的元素(后面的元素)。帧还要保存方法的运行数据,所以还需要保存局部变量表和操作数栈。如下:
public class Frame { Frame lower; // 局部变量表 private LocalVars localVars; // 操作数栈 private OperandStack operandStack; public Frame(int maxLocals, int maxStack) { this.localVars = new LocalVars(maxLocals); this.operandStack = new OperandStack(maxStack); } }
执行方法所需要的局部变量表大小核操作数栈深度是由编译器计算好的,可以从 class 文件 method_info 中的 Code 属性中。
局部变量表
局部变量表是一个可以随机访问的结构,按照下标访问,所以我们可以通过数组实现。根据 JVM 规范,每个元素至少可以容纳一个 int 或引用,两个连续的元素可以容纳一个 long 或 double 类型值。
我们定义一个结构 Slot 表示局部变量表的一个槽位,这样局部变量表就可以被实现为一个 Slot 数组。Slot 可以保存一个 int 或者一个索引,我们分开来存储:
public class Slot { int num; Object ref; }
局部变量表 LocalVars 实现如下:
public class LocalVars { private Slot[] slots; public LocalVars(int maxLocals) { if(maxLocals > 0) { slots = new Slot[maxLocals]; for(int i = 0; i < maxLocals; i ++) { slots[i] = new Slot(); } } } }
接着我们来实现一些数据结构的存储。首先最好实现的是 int 类型,直接将数据保存在槽位的 num 处即可。
public void setInt(int idx, int val) { this.slots[idx].num = val; } public int getInt(int idx) { return slots[idx].num; }
float 类型的数据可以先转换成 int 类型,Float 类中有一个方法 floatToRawIntBits 返回表示浮点数的 int 类型位模式,在取出时可以通过 intBitsToFloat 方法再转换为 float。
public void setFloat(int idx, float val) { this.slots[idx].num = Float.floatToRawIntBits(val); } public Float getFloat(int idx) { int num = this.slots[idx].num; return Float.intBitsToFloat(num); }
long 类型需要拆成两个 int 变量。第一个槽存储低 32 位,第二个槽存储高 32 位。在取出时再组合到一起即可。
public void setLong(int idx, long val) { this.slots[idx].num = (int) (val & 0x000000ffffffffL); this.slots[idx + 1].num = (int) (val >> 32); } public Long getLong(int idx) { long low = this.slots[idx].num & 0x000000ffffffffL; long high = this.slots[idx + 1].num & 0x000000ffffffffL; return (high << 32) | low; }
double 类型可以通过 doubleToRawLongBits 方法转换为 long
类型的位模式,再按照 long 类型存储。取出同理。
public void setDouble(int idx, double val) { setLong(idx, Double.doubleToRawLongBits(val)); } public Double getDouble(int idx) { return Double.longBitsToDouble(getLong(idx)); }
引用值和 int 类似,直接存取即可。
public void setRef(int idx, Object ref) { slots[idx].ref = ref; } public Object getRef(int idx) { return slots[idx].ref; }
根据 JVM 规范,除了以上提到的几种类型,boolean、byte、short 和 char 类型的数据都是直接转换为 int 值处理,不用单独实现。
操作数栈
操作数栈的实现和局部变量表基本一致。使用一个 Slot 数组来保存操作数。size 用于记录栈顶元素的下标。
public class OperandStack { private int size = 0; private Slot[] slots; public OperandStack(int maxStack) { if(maxStack > 0) { slots = new Slot[maxStack]; for(int i = 0; i < maxStack; i ++) { slots[i] = new Slot(); } } } }
后面关于各种类型的入栈和出栈与局部变量表也基本一样,只需要再处理下 size 即可。不做多余解释。
public void pushInt(int val) { slots[size].num = val; size ++; } public int popInt() { size --; return slots[size].num; } public void pushFloat(float val) { slots[size].num = Float.floatToRawIntBits(val); size ++; } public Float popFloat() { size --; int num = this.slots[size].num; return Float.intBitsToFloat(num); } public void pushLong(long val) { slots[size].num = (int) (val & 0x000000ffffffffL); slots[size + 1].num = (int)(val >> 32); size += 2; } public long popLong() { size -= 2; long low = this.slots[size].num & 0x000000ffffffffL; long high = this.slots[size + 1].num & 0x000000ffffffffL; return (high << 32) | low; } public void pushDouble(double val) { pushLong(Double.doubleToRawLongBits(val)); } public Double popDouble() { return Double.longBitsToDouble(popLong()); } public void pushRef(Object ref) { slots[size].ref = ref; size ++; } public Object popRef(){ size --; Object ref = slots[size].ref; slots[size].ref = null; return ref; }
测试
我们修改 Main 类中的 startJVM 方法,在启动时手动创建一个帧,以测试局部变量表和操作数栈。
private static void startJVM(Cmd args) { Frame frame = new Frame(100, 100); test_localVars(frame.localVars()); test_operandStack(frame.operandStack()); }
两个测试方法如下:
private static void test_localVars(LocalVars vars) { vars.setInt(0,100); vars.setInt(1,-100); vars.setLong(2,2997924580L); vars.setLong(4,-2997924580L); vars.setFloat(6, 3.1415926f); vars.setDouble(7, 2.71828182845); vars.setRef(9, null); System.out.println(vars.getInt(0)); System.out.println(vars.getInt(1)); System.out.println(vars.getLong(2)); System.out.println(vars.getLong(4)); System.out.println(vars.getFloat(6)); System.out.println(vars.getDouble(7)); System.out.println(vars.getRef(9)); } private static void test_operandStack(OperandStack ops) { ops.pushInt(100); ops.pushInt(-100); ops.pushLong(2997924580L); ops.pushLong(-2997924580L); ops.pushFloat(3.1415926f); ops.pushDouble(2.71828182845); ops.pushRef(null); System.out.println(ops.popRef()); System.out.println(ops.popDouble()); System.out.println(ops.popFloat()); System.out.println(ops.popLong()); System.out.println(ops.popLong()); System.out.println(ops.popInt()); System.out.println(ops.popInt()); }
其实就是将各种类型的数据存储一遍再取出来。启动后输出结果:
100 -100 2997924580 -2997924580 3.1415925 2.71828182845 null null 2.71828182845 3.1415925 -2997924580 2997924580 -100 100
数据与我们存入时一致!
#Java#