Golang實現JAVA虛擬機器-執行時資料區

橡皮筋兒發表於2023-12-25

原文連結:https://gaoyubo.cn/blogs/8ae1f4ca.html

前置

Golang實現JAVA虛擬機器-解析class檔案

一、執行時資料區概述

JVM學習: JVM-執行時資料區

執行時資料區可以分為兩類:一類是多執行緒共享的,另一類則是執行緒私有的。

  • 多執行緒共享的執行時資料區需要在Java虛擬機器啟動時建立好,在Java虛擬機器退出時銷燬。
    • 物件例項儲存在堆區
    • 類資訊資料儲存在方法區
    • 從邏輯上來講,方法區其實也是堆的一部分。
  • 執行緒私有的執行時資料區則在建立執行緒時才建立,執行緒退出時銷燬。
    • pc暫存器(Program Counter):執行java方法表示:正在執行的Java虛擬機器指令的地址;執行本地方法:pc暫存器無意義
    • Java虛擬機器棧(JVM Stack)。
      • 棧幀(Stack Frame),幀中儲存方法執行的狀態
        • 區域性變數表(Local Variable):存放方法引數和方法內定義的區域性變數。
        • 運算元棧(Operand Stack)等。

虛擬機器實現者可以使用任何垃圾回收算 法管理堆,甚至完全不進行垃圾收集也是可以的。

由於Go本身也有垃圾回收功能,所以可以直接使用Go的垃圾收集器,這大大簡化了工作

二、資料型別概述

Java虛擬機器可以操作兩類資料:基本型別(primitive type)和引用型別(reference type)。

  • 基本型別的變數存放的就是資料本身
    • 布林型別(boolean type)
    • 數字型別 (numeric type)
      • 整數型別(integral type)
      • 浮點數型別(floating-point type)。
  • 引用型別的變數存放的是物件引用,真正的物件資料是在堆裡分配的。
    • 型別:指向類例項
    • 介面型別:用指向實現了該介面的類或陣列例項
    • 陣列型別: 指向陣列例項
    • null:表示該引用不指向任何對 象。

對於基本型別,可以直接在Go和Java之間建立對映關係。
對於引用型別,自然的選擇是使用指標。Go提供了nil,表示空指標,正好可以用來表示null。

三、實現執行時資料區

建立\rtda目錄(run-time data area),建立object.go檔案, 在其中定義Object結構體,程式碼如下:

package rtda
type Object struct {
	// todo
}

本節將實現執行緒私有的執行時資料區,如下圖。下面先從執行緒開始。

3.1執行緒

下建立thread.go檔案,在其中定義Thread結構體,程式碼如下:

package rtda
type Thread struct {
	pc int
	stack *Stack
}
func NewThread() *Thread {...}
func (self *Thread) PC() int { return self.pc } // getter
func (self *Thread) SetPC(pc int) { self.pc = pc } // setter
func (self *Thread) PushFrame(frame *Frame) {...}
func (self *Thread) PopFrame() *Frame {...}
func (self *Thread) CurrentFrame() *Frame {...}

目前只定義了pc和stack兩個欄位。

  • pc欄位代表(pc暫存器)
  • stack欄位是Stack結構體(Java虛擬機器棧)指標

和堆一樣,Java虛擬機器規範對Java虛擬機器棧的約束也相當寬鬆。
Java虛擬機器棧可以是:連續的空間,也可以不連續;可以是固定大小,也可以在執行時動態擴充套件。

  • 如果Java虛擬機器棧有大小限制, 且執行執行緒所需的棧空間超出了這個限制,會導致 StackOverflowError異常丟擲。
  • 如果Java虛擬機器棧可以動態擴充套件,但 是記憶體已經耗盡,會導致OutOfMemoryError異常丟擲。

建立Thread例項的程式碼如下:

func NewThread() *Thread {
	return &Thread{
		stack: newStack(1024),
	}
}

newStack()函式建立Stack結構體例項,它的參數列示要建立的Stack最多可以容納多少幀

PushFrame()PopFrame()方法只是呼叫Stack結構體的相應方法而已,程式碼如下:

func (self *Thread) PushFrame(frame *Frame) {
    self.stack.push(frame)
}
func (self *Thread) PopFrame() *Frame {
    return self.stack.pop()
}

CurrentFrame()方法返回當前幀,程式碼如下:

func (self *Thread) CurrentFrame() *Frame {
	return self.stack.top()
}

3.2虛擬機器棧

用經典的連結串列(linked list)資料結構來實現Java虛擬機器棧,這樣就可以按需使用記憶體空間,而且彈出的也可以及時被Go的垃圾收集器回收。

建立jvm_stack.go檔案,在其中定義Stack結構體,程式碼如下:

package rtda
type Stack struct {
    maxSize uint
    size uint
    _top *Frame
}
func newStack(maxSize uint) *Stack {...}
func (self *Stack) push(frame *Frame) {...}
func (self *Stack) pop() *Frame {...}
func (self *Stack) top() *Frame {...}

maxSize欄位儲存棧的容量(最多可以容納多少幀),size欄位儲存棧的當前大小,_top欄位儲存棧頂指標。newStack()函式的程式碼 如下:

func newStack(maxSize uint) *Stack {
    return &Stack{
       maxSize: maxSize,
    }
}

push()方法把幀推入棧頂,目前沒有實現異常處理,採用panic代替,程式碼如下:

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++
}

pop()方法把棧頂幀彈出:

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
}

top()方法檢視棧頂棧幀,程式碼如下:

// 檢視棧頂元素
func (self *Stack) top() *Frame {
    if self._top == nil {
       panic("jvm stack is empty!")
    }

    return self._top
}

3.3棧幀

建立frame.go檔案,在其中定義Frame結構體,程式碼如下:

package rtda
type Frame struct {
    lower *Frame               //指向下一棧幀
	localVars    LocalVars     // 區域性變數表
	operandStack *OperandStack //運算元棧
}
func newFrame(maxLocals, maxStack uint) *Frame {...}

Frame結構體暫時也比較簡單,只有三個欄位,後續還會繼續完善它。

  • lower欄位用來實現連結串列資料結構
  • localVars欄位儲存區域性變數表指標
  • operandStack欄位儲存運算元棧指標

NewFrame()函式建立Frame例項,程式碼如下:

func NewFrame(maxLocals, maxStack uint) *Frame {
    return &Frame{
       localVars:    newLocalVars(maxLocals),
       operandStack: newOperandStack(maxStack),
    }
}

目前結構如下圖:

3.4區域性變數表

區域性變數表的容量以變數槽(Variable Slot)為最小單位,Java虛擬機器規範並沒有定義一個槽所應該佔用記憶體空間的大小,但是規定了一個槽應該可以存放一個32位以內的資料型別。

在Java程式編譯為Class檔案時,就在方法的Code屬性中的max_locals資料項中確定了該方法所需分配的區域性變數表的最大容量。(最大Slot數量)

區域性變數表是按索引訪問的,所以很自然,可以把它想象成一 個陣列。

根據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值和一個引用值。

這裡將使用第三種方案。建立slot.go檔案,在其中定義Slot結構體, 程式碼如下:

package rtda

type Slot struct {
	num int32
	ref *Object
}

num欄位存放整數,ref欄位存放引用,剛好滿足我們的需求。

用它來實現區域性變數表。建立local_vars.go檔案,在其中定義LocalVars型別,程式碼如下:

package rtda
import "math"
type LocalVars []Slot

定義newLocalVars()函式, 程式碼如下:

func newLocalVars(maxLocals uint) LocalVars {
    if maxLocals > 0 {
       return make([]Slot, maxLocals)
    }
    return nil
}

操作區域性變數表和運算元棧的指令都是隱含型別資訊的。下面給LocalVars型別定義一些方法,用來存取不同型別的變數。
int變數最簡單,直接存取即可

func (self LocalVars) SetInt(index uint, val int32) {
    self[index].num = val
}
func (self LocalVars) GetInt(index uint) int32 {
    return self[index].num
}

float變數可以先轉成int型別,然後按int變數來處理。

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變數則需要拆成兩個int變數。(用兩個slot儲存)

// long consumes two slots
func (self LocalVars) SetLong(index uint, val int64) {
    //後32位
    self[index].num = int32(val)
    //前32位
    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變數可以先轉成long型別,然後按照long變數來處理。

// 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
}

注意,並沒有真的對boolean、byte、short和char型別定義存取方法,這些型別的值都可以轉換成int值類來處理。

下面我們來實現運算元棧。

3.5運算元棧

運算元棧的實現方式和區域性變數表類似。建立operand_stack.go檔案,在其中定義OperandStack結構體,程式碼如下:

package rtda
import "math"
type OperandStack struct {
    size uint
    slots []Slot
}

運算元棧的大小是編譯器已經確定的,所以可以用[]Slot實現。 size欄位用於記錄棧頂位置。
實現newOperandStack()函式,程式碼如下:

func newOperandStack(maxStack uint) *OperandStack {
	if maxStack > 0 {
		return &OperandStack{
			slots: make([]Slot, maxStack),
		}
	}
	return nil
}

需要定義一些方法從運算元棧中彈出,或者往其中推入各種型別的變 量。首先實現最簡單的int變數。

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
}

PushInt()方法往棧頂放一個int變數,然後把size加1。
PopInt() 方法則恰好相反,先把size減1,然後返回變數值。

float變數還是先轉成int型別,然後按int變數處理。

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變數推入棧頂時,要拆成兩個int變數。
彈出時,先彈出 兩個int變數,然後組裝成一個long變數。

// long 佔兩個solt
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變數先轉成long型別,然後按long變數處理。

// 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)
}

彈出引用後,把Slot結構體的ref欄位設定成nil,這樣做是為了幫助Go的垃圾收集器回收Object結構體例項。

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
}

四、區域性變數表和運算元棧例項分析

以圓形的周長公式為例進行分析,下面是Java方法的程式碼。

public static float circumference(float r) {
    float pi = 3.14f;
    float area = 2 * pi * r;
    return area;
}

上面的方法會被javac編譯器編譯成如下位元組碼:

00 ldc #4
02 fstore_1
03 fconst_2
04 fload_1
05 fmul
06 fload_0
07 fmul
08 fstore_2
09 fload_2
10 return

下面分析這段位元組碼的執行。

circumference()方法的區域性變數表大小是3,運算元棧深度是2。
假設呼叫方法時,傳遞給它的引數 是1.6f,方法開始執行前,幀的狀態如圖4-3所示。

第一條指令是ldc,它把3.14f推入棧頂

上面是區域性變數表和運算元棧過去的狀態,最下面是當前狀態。

接著是fstore_1指令,它把棧頂的3.14f彈出,放到#1號區域性變數中

fconst_2指令把2.0f推到棧頂

fload_1指令把#1號區域性變數推入棧頂

fmul指令執行浮點數乘法。它把棧頂的兩個浮點數彈出,相乘,然後把結果推入棧頂

fload_0指令把#0號區域性變數推入棧頂

fmul繼續乘法計算

fstore_2指令把運算元棧頂的float值彈出,放入#2號區域性變數表

最後freturn指令把運算元棧頂的float變數彈出,返回給方法調 用者

五、測試

main()方法中修改startJVM:

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())
}

相關文章