JVM的簡單實現


本文介紹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虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章