原文連結:https://gaoyubo.cn/blogs/8ae1f4ca.html
前置
一、執行時資料區概述
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虛擬機器棧有大小限制, 且執行執行緒所需的棧空間超出了這個限制,會導致
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())
}