項目地址:https://github.com/gongxianshengjiadexiaohuihui
後面的筆記將不會區分go很java,也會改變以前大篇幅的寫代碼,更加註重對思想的講解,但是兩種語言的代碼會同步更新
我們只到cpu是通過執行一條條設定好的指令,來指揮我們的電腦,同樣虛擬機的工作也是執行一條條指令,那麼我們的指令放在那裏呢?如果有印象的話,實在MemberInfo的Code屬性中,我們需要讀取裏面的字節碼然後,去執行指令。字節碼中存放編碼後的java虛擬機指令。每一條指令都以一個單字節的操作碼開頭,這就是字節碼名稱的由來。
由於只使用一個字節表示操作碼,java虛擬機最多隻能支持256條指令。到第八版爲止。java虛擬機規範已經定義了205條指令構成了java虛擬機的指令集。和彙編語言類似,java虛擬機規範給每個操作碼都指定了一個助記符。比如0x00這條指令,助記符是nop。
java虛擬機使用的是變長指令,操作碼後面可以跟零字節或多字節。如果把指令想象成函數的話。操作數就是它的參數。爲了讓編碼後的字節碼更加緊湊,很多操作碼本身隱含了操作數,比如把常數0推入操作數棧的助記就是iconst_0
java虛擬機規範把已經定義的205條指令按用途分爲了11類,
指令涉及的操作有 取指令 解析指令 執行指令,解析指令我們可以寫一個工廠類來完成,取指令和執行指令我們可以抽象出一個接口來
package instructions.base;
import rtda.Frame;
/**
* @Author:ggp
* @Date:2019/2/28 14 02
* @Description:指令的接口,每條指令都要實現
*/
public interface Instruction {
/**
* 取指令操作
* @param reader
*/
void fetchOperands(ByteCodeReader reader);
/**
* 執行指令操作
* @param frame
*/
void execute(Frame frame);
}
同時指令按照類型還能分爲沒有操作數的指令 操作數是單字節的需要訪問索引,操作數是雙字節的,需要訪問運行時常量池。還有跳轉指令
拿使用頻率最高的無操作數指令的抽象父類來舉例
package instructions.base;
/**
* @Author:ggp
* @Date:2019/3/9 14 18
* @Description:沒有操作數的指令,操作數往往隱藏在指令中
*/
public abstract class NoOperandsInstruction implements Instruction{
@Override
public void fetchOperands(ByteCodeReader reader){
}
}
然後下面的幾種指令集只需要繼承上面的幾個抽象父類,實現自己的執行指令方法即可
常量指令
常量指令把常量推入操作數棧頂。常量可以來自三個地方:隱含在操作碼裏、操作數和運行時常量池
加載指令 loads
加載指令是根據索引去局部變量表中拿取變量放入操作數棧中,索引來自操作數,或隱藏在操作碼中
存儲指令 stores
存儲指令和加載指令相反,從操作數棧彈出棧頂元素,然後根據索引存儲在局部變量表中,索引來自操作數或隱藏在操作碼中
操作數棧指令 stack
pop系列,彈出棧頂slot
dup系列,複製棧頂變量
swap系列,交換棧頂變量
數學指令 math
彈出操作數棧的兩個變量,進行數學運算,包括加減乘除取餘取反位移還有邏輯運算
轉換指令 conversions
彈出操作數棧的棧頂元素,進行四種基本類型的轉換 int long double float
比較指令 comparisons
分兩種,一種是把比較結果推入操作數棧頂,另一種是根據比較結果跳轉
控制指令 control
跳轉指令和switch指令
擴展指令 extended
需要訪問局部變量表的指令,索引以uint8的形式存在字節碼中,對於大部分方法來說,局部變量表大小一般不會超過256,所以用一個自己就可以了,但是如果有方法的局部變量表超過這限制時,java虛擬機規範定義了wide指令來擴展上面的指令
引用指令 references
本節不涉及
保留指令 reserved
保留指令一共有三條。其中一條是留給調試器的,用於實現斷點,操作碼是202,助記符是breakpoint。另外兩條是留給java虛擬機內部使用。分別是254和266,助記符是impdep1和impdep2,這三條指令不允許出現在class文件中。
解釋器
其實我們上面已經說了解釋器的工作 ,取指令 解析指令 執行指令
我們看一下實現
package main
import (
"fmt"
"jvmgo/classfile"
"jvmgo/instructions"
"jvmgo/instructions/base"
"jvmgo/rtda"
)
//解釋器
func interpret(methodInfo *classfile.MemberInfo ){
codeAtrr := methodInfo.CodeAttribute()
maxLocals := codeAtrr.MaxLocals()
maxStack := codeAtrr.MaxStack()
bytecode := codeAtrr.Code()
//回憶一下,new線程的時候,會創建一個1024幀大小的stack
thread := rtda.NewThread()
//暫時只推進一幀,也就是隻執行一個方法
frame := thread.NewFrame(maxLocals, maxStack)
thread.PushFrame(frame)
//defer是Golang中的關鍵字,常用來釋放資源,會在函數返回之前進行調用。如果有多個defer表達式,調用順序類似於棧,後進先出
//defer函數調用的執行時機是外層函數設置返回值之後,並且在即將返回之前
defer catchErr(frame)
loop(thread, bytecode)
}
func catchErr(frame *rtda.Frame){
//go中可以拋出一個panic異常,然後通過在defer中通過recover捕獲這個異常,然後正常處理
//在一個主進程中,多個go線程處理邏輯的結構中,這個很重要,如果不用recover捕獲panic異常,會導致整個進程出錯中斷
if r := recover(); r != nil{
fmt.Printf("LocalVars:%v\n", frame.LocalVars())
fmt.Printf("OperandStack:%v\n", frame.OperandStack())
panic(r)
}
}
//循環執行 計算pc,解碼指令,執行指令
func loop(thread *rtda.Thread, bytecode []byte){
fmt.Printf("**\n")
frame := thread.PopFrame()
reader := &base.ByteCodeReader{}
for{
//取指令
pc := frame.NextPC();
thread.SetPC(pc);
//主要針對跳轉指令
reader.Reset(bytecode,pc)
opcode := reader.ReadUint8()
//解碼
inst := instructions.NewInstruction(opcode);
inst.FetchOperands(reader)
frame.SetNextPC(reader.PC())
//執行
fmt.Printf("pc:%2d inst:%T %v\n",pc, inst, inst)
inst.Execute(frame)
}
}