原文鏈接: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)等。
- 棧幀(Stack Frame),幀中保存方法執行的狀態
虛擬機實現者可以使用任何垃圾回收算 法管理堆,甚至完全不進行垃圾收集也是可以的。
由于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虛擬機棧有大小限制, 且執行線程所需的棧空間超出了這個限制,會導致
*Error
異常拋出。 - 如果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.*Error")
}
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())
}