代码、内容参考来自于张秀宏大佬的自己动手写Java虚拟机 (Java核心技术系列)以及尚硅谷宋红康:JVM全套教程。
1.运行时数据区概述

在运行Java程序时,Java虚拟机需要使用内存来存放各式各样的数据。Java虚拟机规范把这些内存区域叫作运行时数据区。运行时数据区可以分为两类:
一类是多线程共享的,另一类则是线程私有的。多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁。线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。
多线程共享的内存区域主要存放两类数据:类数据和类实例(也就是对象)。对象数据存放在堆(Heap)中,类数据存放在方法区(Method Area)中。堆由垃圾收集器定期清理,所以程序员不需要关心对象空间的释放。类数据包括字段和方法信息、方法的字节码、运行时常量池,等等。从逻辑上来讲,方法区其实也是堆的一部分。
线程私有的运行时数据区用于辅助执行Java字节码。每个线程都有自己的pc寄存器(Program Counter)和Java虚拟机栈(JVM Stack)。Java虚拟机栈又由栈帧(Stack Frame,后面简称帧)构成,帧中保存方法执行的状态,包括局部变量表(Local Variable)和操作数栈(Operand Stack)等。在任一时刻,某一线程肯定是在执行某个方法。这个方法叫作该线程的当前方法;执行该方法的帧叫作线程的当前帧;声明该方法的类叫作当前类。如果当前方法是Java方法,则pc寄存器中存放当前正在执行的Java虚拟机指令的地址,否则,当前方法是本地方法,pc寄存器中的值没有明确定义。
如图:

Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
- 每个线程:独立包括程序计数器、栈、本地栈。
- 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)。

在整个JVM优化中,线程里的结构没有什么可优化的点,像虚拟机栈,只有进栈和出栈两个操作。主要是堆区和方法区的优化。当然也存在OutotMemoryError(内存溢出)。
每个JVM只有一个Runtime实例。即为运行时环境,相当于内存结构的中间的那个框框:运行时环境。
Java虚拟机规范对于运行时数据区的规定是相当宽松的。以堆为例:堆可以是连续空间,也可以不连续。堆的大小可以固定,也可以在运行时按需扩展 。虚拟机实现者可以使用任何垃圾回收算法管理堆,甚至完全不进行垃圾收集也是可以的。Go本身也有垃圾回收功能,《自己动手写Java虚拟机》的作者直接使用Go的堆和垃圾收集器,所以我也不打算写了。以后有机会再回来写吧。
2.数据类型
Java虚拟机可以操作两类数据:基本类型(primitive type)和引用类型(reference type)。基本类型的变量存放的就是数据本身,引用类型的变量存放的是对象引用,真正的对象数据是在堆里分配的。这里所说的变量包括类变量(静态字段)、实例变量(非静态字段)、数组元素、方法的参数和局部变量,等等。基本类型可以进一步分为布尔类型(boolean type)和数字类型(numeric type)
数字类型又可以分为整数类型(integral type)和浮点数类型(floating-point type)。引用类型可以进一步分为3种:类类型、接口类型和数组类型。类类型引用指向类实例,数组类型引用指向数组实例,接口类型引用指向实现了该接口的类或数组实例。引用类型有一个特殊的值——null,表示该引用不指向任何对象。
Go语言提供了非常丰富的数据类型,包括各种整数和两种精度的浮点数。Java和Go的浮点数都采用IEEE 754规范 。对于基本类型,可以直接在Go和Java之间建立映射关系。对于引用类型,使用指针。Go提供了nil,表示空指针,正好可以用来表示null。
由于要到第6章才开始实现类和对象,所以先定义一个临时的结构体,用它来表示对象。
新建/rtda目录下创建object.go,在其中定义Object结构体,
代码如下:
package rtda
type Object struct {
// todo
}
Java虚拟机数据类型

还有一种基本类型是returnAddress,它和jsr、ret、ret_w指令一起,用来实现finally子句。不过从Java 6开始,Oracle的Java编译器已经不再使用这三条指令了。
下面开始实现线程私有的运行时数据区
3.线程
整体代码
在jvm\rtda目录下创建thread.go文件,在其中定义Thread结构体,代码如下:
package rtda
/*
JVM
Thread
pc
Stack
Frame
LocalVars
OperandStack
*/
type Thread struct {
pc int // the address of the instruction currently being executed
stack *Stack
// todo
}
func NewThread() *Thread {
return &Thread{
stack: newStack(1024),
}
}
func (self *Thread) PC() int {
return self.pc
}
func (self *Thread) SetPC(pc int) {
self.pc = pc
}
func (self *Thread) PushFrame(frame *Frame) {
self.stack.push(frame)
}
func (self *Thread) PopFrame() *Frame {
return self.stack.pop()
}
func (self *Thread) CurrentFrame() *Frame {
return self.stack.top()
}
代码解释
目前只定义了pc和stack两个字段。pc字段无需解释,stack字段是Stack结构体(Java虚拟机栈)指针。Stack结构体在4.3.2节介绍。
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。
- 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。
- 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈那Java虚拟机将会抛出一个 OutOfMemonyError 异常。
NewThread函数
NewThread函数创建Thread实例:

Getter方法

newStack函数
newStack()函数创建Stack结构体实例,它的参数表示要创建的Stack最多可以容纳多少帧,4.3.2节将给出这个函数的代码。这里暂时将它赋值为1024。

PushFrame和PopFrame方法
PushFrame()和PopFrame()方法只是调用Stack结构体的相应方法而已:

CurrentFrame方法
CurrentFrame()方法返回当前帧:

4.Java虚拟机栈
如前所述,Java虚拟机规范对Java虚拟机栈的约束非常宽松。我们用经典的链表(linked list)数据结构来实现Java虚拟机栈,这样栈就可以按需使用内存空间,而且弹出的帧也可以及时被Go的垃圾收集器回收。
整体代码
在\jvm\rtda目录下创建jvm_stack.go文件,在其中定义Stack结构体:
package rtda
// jvm stack
type Stack struct {
maxSize uint
size uint
_top *Frame // stack is implemented as linked list
}
func newStack(maxSize uint) *Stack {
return &Stack{
maxSize: maxSize,
}
}
func (self *Stack) push(frame *Frame) {
if self.size >= self.maxSize {
panic("java.lang.StackOverflowError")
}
if self._top != nil {
frame.lower = self._top
}
self._top = frame
self.size++
}
func (self *Stack) pop() *Frame {
if self._top == nil {
panic("jvm stack is empty!")
}
top := self._top
self._top = top.lower
top.lower = nil
self.size--
return top
}
func (self *Stack) top() *Frame {
if self._top == nil {
panic("jvm stack is empty!")
}
return self._top
}代码解释
结构体
maxSize字段保存栈的容量(最多可以容纳多少帧),size字段保存栈的当前大小,_top字段保存栈顶指针。

newStack函数
newStack函数,传入大小,创建一个栈。

push方法
push方法把帧推入栈顶,栈的push操作,如果栈已经满了,按照Java虚拟机规范,应该抛出StackOverflowError异常。在第10章才会讨论异常,这里先调用panic()函数终止程序执行。

pop方法
pop方法把栈顶帧弹出。

如果此时栈是空的,肯定是我们的虚拟机有bug,调用panic()函数终止程序执行即可。top()方法只是返回栈顶帧,但并不弹出。

5.帧
整体代码
在\jvm\rtda目录下创建frame.go文件,在其中定义Frame结构体,代码如下:
package rtda
// stack frame
type Frame struct {
lower *Frame // stack is implemented as linked list
localVars LocalVars
operandStack *OperandStack
// todo
}
func NewFrame(maxLocals, maxStack uint) *Frame {
return &Frame{
localVars: newLocalVars(maxLocals),
operandStack: newOperandStack(maxStack),
}
}
// getters
func (self *Frame) LocalVars() LocalVars {
return self.localVars
}
func (self *Frame) OperandStack() *OperandStack {
return self.operandStack
}
代码解释
结构体
Frame结构体暂时也比较简单,只有三个字段,后续章节还会继续完善它。lower字段用来实现链表数据结构,localVars字段保存局部变量表指针,operandStack字段保存操作数栈指针。

NewFrame函数
NewFrame函数创建Frame实例:

执行方法所需的局部变量表大小和操作数栈深度是由编译器预先计算好的,存储在class文件method_info结构的Code属性中。
Thread、Stack和Frame结构体的代码都已经给出了,Java虚拟机栈的链表结构,如图

6.局部变量表
关于Slot的理解
- 参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
- 局部变量表,最基本的存储单元是slot (变量槽)。
- 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
- 在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型 (long和double)占用两个slot。byte 、short 、char 在存储前被转换为int,boolean 也被转换为int,0 表示false ,非0 表示true。long 和double 则占据两个slot。
- JVM会为局部变量表中的每一个Slot都分配问到局部变量表中指定的局部变量值
方问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。 - 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序(声明顺序)被复制制到局部变量表中的每一个Slot上。
- 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量就使用第一个索引)。
局部变量表是按索引访问的,所以很自然,可以把它想象成一个数组。根据Java虚拟机规范,这个数组的每个元素至少可以容纳一个int或引用值,两个连续的元素可以容纳一个long或double值。那么使用哪种Go语言数据类型来表示这个数组呢?最容易想到的是[]int。Go的int类型因平台而异,在64位系统上是int64,在32位系统上是int32,总之足够容纳Java的int类型。另外它和内置的uintptr类型宽度一样,所以也足够放下一个内存地址。通过unsafe包可以拿到结构体实例的地址,如下所示:
obj := &Object{}
ptr := uintptr(unsafe.Pointer(obj))
ref := int(ptr)但遗憾的是,Go的垃圾回收机制并不能有效处理uintptr指针。也就是说,如果一个结构体实例,除了uintptr类型指针保存它的地址之外,其他地方都没有引用这个实例,它就会被当作垃圾回收。另外一个方案是用[]interface{}类型,这个方案在实现上没有问题,只是写出来的代码可读性太差。第三种方案是定义一个结构体,让它可以同时容纳一个int值和一个引用值。这里将使用第三种方案。
整体代码
在\rtda目录下创建slot.go文件,在其中定义Slot结构体,代码如下:
package rtda
type Slot struct {
num int32
ref *Object
}
在\rtda目录下创建local_vars.go文件,在其中定义LocalVars类型,代码如下:
package rtda
import "math"
type LocalVars []Slot
func newLocalVars(maxLocals uint) LocalVars {
if maxLocals > 0 {
return make([]Slot, maxLocals)
}
return nil
}
func (self LocalVars) SetInt(index uint, val int32) {
self[index].num = val
}
func (self LocalVars) GetInt(index uint) int32 {
return self[index].num
}
func (self LocalVars) SetFloat(index uint, val float32) {
bits := math.Float32bits(val)
self[index].num = int32(bits)
}
func (self LocalVars) GetFloat(index uint) float32 {
bits := uint32(self[index].num)
return math.Float32frombits(bits)
}
// long consumes two slots
func (self LocalVars) SetLong(index uint, val int64) {
self[index].num = int32(val)
self[index+1].num = int32(val >> 32)
}
func (self LocalVars) GetLong(index uint) int64 {
low := uint32(self[index].num)
high := uint32(self[index+1].num)
return int64(high)<<32 | int64(low)
}
// double consumes two slots
func (self LocalVars) SetDouble(index uint, val float64) {
bits := math.Float64bits(val)
self.SetLong(index, int64(bits))
}
func (self LocalVars) GetDouble(index uint) float64 {
bits := uint64(self.GetLong(index))
return math.Float64frombits(bits)
}
func (self LocalVars) SetRef(index uint, ref *Object) {
self[index].ref = ref
}
func (self LocalVars) GetRef(index uint) *Object {
return self[index].ref
}
代码解释
结构体
num字段存放整数,ref字段存放引用,用它来实现局部变量表。

newLocalVars
继续编辑local_vars.go文件,在其中定义newLocalVars()函数,newLocalVars()函数创建LocalVars实例,代码比较简单,这里就不多解释了。

LocalVars类型一些方法
LocalVars类型定义一些方法,用来存取不同类型的变量。int变量最简单,直接存取即可。

float变量可以先转成int类型,然后按int变量来处理。math包的Float32bits返回val的IEEE 754二进制表示形式(但是返回一个uint32类型,要按int变量来处理,就要转换成int32)。而Float32frombits返回与IEEE 754二进制表示b对应的浮点数(同理,因为按int32存储进去,所以要换成uint32出来,才能使用Float32frombits)。

long变量则需要拆成两个int变量。

double变量可以先转成long类型,然后按照long变量来处理。

最后是引用值,也比较简单,直接存取即可。

我们并没有真的对boolean、byte、short和char类型定义存取方法,这些类型的值都可以转换成int值类来处理。下面们来实现操作数栈。
7.操作数栈
整体代码
操作数栈的实现方式和局部变量表类似。在ch04\rtda目录下创建operand_stack.go文件,在其中定义OperandStack结构体,代码如下:
package rtda
import "math"
type OperandStack struct {
size uint
slots []Slot
}
func newOperandStack(maxStack uint) *OperandStack {
if maxStack > 0 {
return &OperandStack{
slots: make([]Slot, maxStack),
}
}
return nil
}
func (self *OperandStack) PushInt(val int32) {
self.slots[self.size].num = val
self.size++
}
func (self *OperandStack) PopInt() int32 {
self.size--
return self.slots[self.size].num
}
func (self *OperandStack) PushFloat(val float32) {
bits := math.Float32bits(val)
self.slots[self.size].num = int32(bits)
self.size++
}
func (self *OperandStack) PopFloat() float32 {
self.size--
bits := uint32(self.slots[self.size].num)
return math.Float32frombits(bits)
}
// long consumes two slots
func (self *OperandStack) PushLong(val int64) {
self.slots[self.size].num = int32(val)
self.slots[self.size+1].num = int32(val >> 32)
self.size += 2
}
func (self *OperandStack) PopLong() int64 {
self.size -= 2
low := uint32(self.slots[self.size].num)
high := uint32(self.slots[self.size+1].num)
return int64(high)<<32 | int64(low)
}
// double consumes two slots
func (self *OperandStack) PushDouble(val float64) {
bits := math.Float64bits(val)
self.PushLong(int64(bits))
}
func (self *OperandStack) PopDouble() float64 {
bits := uint64(self.PopLong())
return math.Float64frombits(bits)
}
func (self *OperandStack) PushRef(ref *Object) {
self.slots[self.size].ref = ref
self.size++
}
func (self *OperandStack) PopRef() *Object {
self.size--
ref := self.slots[self.size].ref
self.slots[self.size].ref = nil
return ref
}
代码解释
结构体
操作数栈的大小是编译器已经确定的,所以可以用[]Slot实现。size字段用于记录栈顶位置。

newOperandStack函数
继续编辑operand_stack.go,在其中实现newOperandStack()函数,代码也比较简单,在此就不多解释了。和局部变量表类似。

从弹出或推入各种类型的变量
需要定义一些方法从操作数栈中弹出,或者往其中推入各种类型的变量。先看最简单的int变量。PushInt()方法往栈顶放一个int变量,然后把size加1。PopInt()方法则恰好相反,先把size减1,然后返回变量值。

float变量还是先转成int类型,然后按int变量处理。

把long变量推入栈顶时,要拆成两个int变量。弹出时,先弹出两个int变量,然后组装成一个long变量。

double变量先转成long类型,然后按long变量处理。

最后看引用类型PushRef()方法比较简单,此处不做太多解释。PopRef()方法需要说明一点:弹出引用后,把Slot结构体的ref字段设置成nil,这样做是为了帮助Go的垃圾收集器回收Object结构体实例。至此,局部变量表和操作数栈都准备好了。:

仅通过代码来理解它们可能不是很直观,下面我们将通过一个具体的例子来分析局部变量表和操作数的使用。
8.局部变量表和操作数栈实例分析
public void testAddOperation() {
//byte、short、char、boolean:都以int型来保存
byte i = 15;
int j = 8;
int k = i + j;
}
具体过程如下(学过计算机组成原理与数据结构应该挺容易理解):








在这过程中,操作数栈的最大深度是2

9.测试代码
这里简单测试局部变量表和操作数栈的用法。
打开ch04\main.go文件,代码如下:
package main
import (
"fmt"
"jvmgo/ch04/rtda"
)
func main() {
cmd := parseCmd()
if cmd.versionFlag {
fmt.Println("version 0.0.1")
} else if cmd.helpFlag || cmd.class == "" {
printUsage()
} else {
startJVM(cmd)
}
}
func startJVM(cmd *Cmd) {
frame := rtda.NewFrame(100, 100)
testLocalVars(frame.LocalVars())
testOperandStack(frame.OperandStack())
}
func testLocalVars(vars rtda.LocalVars) {
vars.SetInt(0, 100)
vars.SetInt(1, -100)
vars.SetLong(2, 2997924580)
vars.SetLong(4, -2997924580)
vars.SetFloat(6, 3.1415926)
vars.SetDouble(7, 2.71828182845)
vars.SetRef(9, nil)
println(vars.GetInt(0))
println(vars.GetInt(1))
println(vars.GetLong(2))
println(vars.GetLong(4))
println(vars.GetFloat(6))
println(vars.GetDouble(7))
println(vars.GetRef(9))
}
func testOperandStack(ops *rtda.OperandStack) {
ops.PushInt(100)
ops.PushInt(-100)
ops.PushLong(2997924580)
ops.PushLong(-2997924580)
ops.PushFloat(3.1415926)
ops.PushDouble(2.71828182845)
ops.PushRef(nil)
println(ops.PopRef())
println(ops.PopDouble())
println(ops.PopFloat())
println(ops.PopLong())
println(ops.PopLong())
println(ops.PopInt())
println(ops.PopInt())
}
main方法不变,修改startJVM方法,代码如下:

testLocalVars函数测试局部变量,代码如下:

testOperandStack函数测试操作数栈,代码如下:

打开命令行窗口,执行下面的命令编译本章代码:
go install jvmgo\ch04
编译成功后,在D:\go\workspace\bin目录下出现ch04.exe文件。
ch04 test

10.参考
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机
张秀宏:自己动手写Java虚拟机 (Java核心技术系列)


