本文介紹java虛擬機的一些知識,並以jvmgo爲例介紹一些虛擬機的簡單實現。jvmgo是用Go語言實現的java虛擬機,其作者說這個項目的主要目的是學習Go和JVM,所以只是一個toy,對於破除JVM的神祕感還是很有幫助的。
class類文件結構
使用java編譯器(java程序用javac,Groovy程序用groovyc編譯器)可以把java代碼編譯位存儲字節碼的class文件,虛擬機並不關心class文件的來源是何種語言。這種做法達到了語言無關性的目的。另外有各種可以運行在不同操作系統上的虛擬機,都可以載入和執行同一種平臺無關的字節碼,實現了平臺無關性。
class文件是一組以8位字節爲基礎單位的二進制流,佔用8位字節以上空間的數據項時以大端方式存儲,最高位字節在地址最低位。
Class文件格式採用下面僞結構來存儲數據,只有兩種數據類型:無符號數和表。無符號數可以作爲指向表的索引,或者bitmask。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
class字節碼文件可以用圖形化工具classpy查看,比命令行工具javap更加方便參看。效果如下:
在jvmgo中用ClassFile結構體表示,把.class文件以字節流的方式讀出,然後填到這個結構體中。
calss文件格式詳情可以看《Java虛擬機規範》和jvm相關文檔:The class File Format.
這裏簡單舉幾個例子。
方法表
一個方法用如下數據結構表示後綴_index表示是指向常量池的索引。
name_index指出了方法名。
descriptor_index指出了方法返回值和參數列表信息,是java中方法重載的關鍵。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
如下是javac編譯器爲類自動生成的<init>
默認構造函數,它的名稱索引和描述符索引分別指向常量池中對應的位置。
方法表第0項:
常量池第12、13項:
屬性表
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
在Class文件、字段表、方法表都可以攜帶自己的屬性表結合,用於描述某些場景專有的信息。
屬性是可以擴展的,不同的虛擬機實現可以定義自己的屬性類型。由於這個原因,Java虛擬機規範沒有使用tag,而是使用屬性名來區別不同的屬性
Code屬性中存放字節碼等方法相關信息。
Code是變長屬性,只存在於method_info結構中。在classpy中觀察main方法的code屬性如下,其中max_stack代表了操作數棧(Operand Stacks)深度的最大值。max_locaks代表了局部變量表所需的存儲空間(包括方法參數),code_length和code用來存儲java程序編譯後生成的字節碼指令。
運行時數據區
在運行Java程序時,虛擬機需要使用內存來存放各種的數據,這個內存區域就是運行時數據區。
多線程共享的內存區域主要存放兩類數據:類數據和類實例 (也就是對象Object)。對象數據存放在堆(Heap)中,類數據存放在方法區 (Method Area)中。堆由垃圾收集器GC定期清理。類數據包括字段和方法信息、方法的字節碼、 運行時常量池,等等。
線程私有的運行時數據區用於輔助執行Java字節碼。每個線程都有自己的pc寄存器(Program Counter)和Java虛擬機棧(JVM Stack)。Java虛擬機棧又由棧幀(Stack Frame)構成,幀中保存方法執行的狀態,包括局部變量表(Local Variable)和操作數棧(Operand Stack)等。如果當前方法是Java方法,則 pc寄存器中存放當前正在執行的Java虛擬機指令的地址,否則,當前方法是本地方法,pc寄存器中的值沒有明確定義。
解釋器
jvmgo中虛擬機字節碼執行引擎代碼如下:
func interpret(method *heap.Method) {
thread := rtda.NewThread()
frame := thread.NewFrame(method)
thread.PushFrame(frame)
loop(thread)
}
func loop(thread *rtda.Thread) {
reader := &base.BytecodeReader{}
for {
frame := thread.CurrentFrame()
pc := frame.NextPC()
thread.SetPC(pc)
// decode
reader.Reset(frame.Method().Code(), pc)
opcode := reader.ReadUint8()
inst := instructions.NewInstruction(opcode)
inst.FetchOperands(reader)
frame.SetNextPC(reader.PC())
// execute
inst.Execute(frame)
if thread.IsStackEmpty() {
break
}
}
}
interpret()法的參數是MemberInfo指針,調用MemberInfo結 構體的CodeAttribute()法可以獲取它的Code屬性,從class文件結構中得到bytecode、maxstack等信息後,創建一個Frame,在一個loop中不停循環解釋字節碼。
每個指令是一個u1類型的單字節,當虛擬機讀取到code中的一個字節碼時,就可以對應找出這個字節碼代表什麼指令,並且可以知道這條指令後面是否需要跟隨參數,以及參數應當如何理解。
如iload指令根據第一個操作數作爲索引從局部變量表取出一個int值,然後push到操作數棧。
指令比較多,做個總結的話,無非是從操作數棧或者局部變量表取出來,算一算,把結果再放回運行時數據區,如果遇到跳轉指令就改變下frame上的pc。
類加載機制
虛擬機的類加載機制:虛擬機把class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型。
java代碼在進行javac編譯的時候,並沒有鏈接這一步驟,而是在虛擬機加載Class文件的時候進行動態鏈接。所以在Class文件中不會保存各個方法、字段的最終內存佈局信息,當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析翻譯到具體的內存地址之中。
在jvmgo中有class_loader的parseClass方法完成類和字段符號引用解析。
類的加載大致可以分爲三個步驟:首先找到class文件並把數據讀取到內存;然後解析class文件,生成虛擬機可以使用的類數據,並放入方法區;最後進行鏈接。
jvmgo中相關實現如下:
type ClassLoader struct {
cp *classpath.Classpath
classMap map[string]*Class // loaded classes
}
func (self *ClassLoader) loadNonArrayClass(name string) *Class {
data, entry := self.readClass(name)
class := self.defineClass(data)
link(class)
return class
}
// jvms 5.3.5
func (self *ClassLoader) defineClass(data []byte) *Class {
class := parseClass(data) //把class文件數據轉換成Class結構體
class.loader = self
resolveSuperClass(class) //解析類符號引用
resolveInterfaces(class)
self.classMap[class.name] = class
return class
}
// jvms 5.4.3.1
func resolveSuperClass(class *Class) {
if class.name != "java/lang/Object" {
class.superClass = class.loader.LoadClass(class.superClassName)
}
}
func link(class *Class) {
verify(class)
prepare(class) //準備階段主要是給類變量分配空間並給予初始值
}
func prepare(class *Class) {
calcInstanceFieldSlotIds(class)
calcStaticFieldSlotIds(class) //計算並分配靜態變量所需內存
allocAndInitStaticVars(class)
}
方法調用
java虛擬機提供了5條方法調用字節碼指令:
invokestatic指令:調用靜態方法。
invokespecial指令:調用無須動態綁定的實例方法,包括構造函數、私有方法和通過super 關鍵字調用的超類方法。
invokevirtual指令:調用所有虛方法。
invokeinterface指令:調用接口方法,會在運行時再確定一個實現此接口的對象。
invokedynamic指令:先在運行時動態解析出調用點限定符所引用的方法,然後再自行該方法,分派的邏輯是由用戶所設定的引導方法決定的。
方法調用參數傳遞如下,對於實例方法,Java編譯器會在參數列表的前面添加一個參數,這個隱藏的參數就是this引用。
依次把這n個變量從調用者的操作數棧中彈出,放進被調用方法的局部變量表中,參數傳遞就完成了
在定位到需要調用的方法之後,Java虛擬機要給這個方法創建 一個新的幀並把它推入Java虛擬機棧頂,然後傳遞參數。
func InvokeMethod(invokerFrame *rtda.Frame, method *heap.Method) {
thread := invokerFrame.Thread()
newFrame := thread.NewFrame(method)
thread.PushFrame(newFrame)
argSlotCount := int(method.ArgSlotCount())
if argSlotCount > 0 {
for i := argSlotCount - 1; i >= 0; i-- {
slot := invokerFrame.OperandStack().PopSlot()
newFrame.LocalVars().SetSlot(uint(i), slot)
}
}
解釋器的loop下一個循環就會從New Frame的開頭開始執行。
實例化對象
實例化對象主要通過new指令。
new指令的操作數是一個uint16索引,來自字節碼。通過這個索引,
可以從當前類的運行時常量池中找到一個類符號引用。
解析這個類符號引用,拿到類數據,然後創建對象(根據類實例變量的個數分配空間),並把對象引用推入棧頂,new指令的工作就完成了。
// Create new object
type NEW struct{ base.Index16Instruction }
func (self *NEW) Execute(frame *rtda.Frame) {
cp := frame.Method().Class().ConstantPool()
classRef := cp.GetConstant(self.Index).(*heap.ClassRef)
class := classRef.ResolvedClass()
if !class.InitStarted() { //類初始化
frame.RevertNextPC()
base.InitClass(frame.Thread(), class)
return
}
ref := class.NewObject()
frame.OperandStack().PushRef(ref)
}
//創建對象
func newObject(class *Class) *Object {
return &Object{
class: class,
fields: newSlots(class.instanceSlotCount),
}
}
比如對於如下的java代碼
public MyObject(int a) {
this.instanceVar = a;
}
public MyObject(int a,int b) {
a = a+b;
this.instanceVar = a;
}
MyObject myObj = new MyObject(100); // new
編譯後,再用javap反編譯如下:
3: new #4 // class jvmgo/book/ch06/MyObject
6: dup
7: bipush 100
9: invokespecial #5 // Method "<init>":(I)V
先調用new指令,開闢一個Object的內存空間,再調用構造函數方法,這裏jvm將通過索引5找到相關的構造函數
總結
jvmgo還實現了數組和字符串、本地方法調用、反射機制、自動裝箱和拆箱、異常處理,感興趣的看看。
參考
https://github.com/zxh0/jvmgo
https://github.com/zxh0/classpy
《Java虛擬機規範》
《深入理解java虛擬機》
《自己動手寫Java虛擬機》