[JVM] - 一份<自己動手寫Java虛擬機>的測試版


go語言下載

配置GOROOT(一般是自動的),配置GOPATH(如果想自己改的話)

參照<自己動手寫Java虛擬機>

> 第一章 指令集和解釋器

生成了ch01.exe文件

這里還生成了一個gopkgs.exe文件

執行以上操作,這里說明:go開發java虛擬機實際上這段模擬的是命令行在安裝好java JDK后的一些輸入,比如查看java的version信息.

這里已經在代碼中寫好了.

main.go :

package main

import "fmt"

func main() {

    cmd := parseCmd()
    if cmd.versionFlag {
        fmt.Println("version 0.0.1")
    }else if cmd.helpFlag || cmd.class == "" {
        printUsage()
    }else{
        startJVM(cmd)
    }
}

func startJVM(cmd * Cmd){
    fmt.Printf("classpath:%s class:%s args:%v\n",cmd.cpOption,cmd.class,cmd.args)
}

cmd.go :

package main

import "flag"
import "fmt"
// import "os"

type Cmd struct {

    helpFlag bool
    versionFlag bool
    cpOption string
    class string
    args []string
}

func parseCmd() *Cmd {
    cmd := &Cmd{}
    //首先設置flag.Usage變量,把printUsage()函數賦值給它
    flag.Usage = printUsage 
    //然后調用flag包提供的各種Var()函數設置需要解析的選項
    flag.BoolVar(&cmd.helpFlag,"help",false,"print help message")
    flag.BoolVar(&cmd.helpFlag,"?",false,"print help message")
    flag.BoolVar(&cmd.versionFlag,"version",false,"print version and exit")
    flag.StringVar(&cmd.cpOption,"classpath","","classpath")
    flag.StringVar(&cmd.cpOption,"cp","","classpath")
    //接着調用Parse()函數解析選項.如果Parse()函數解析失敗,它就調用printUsage()函數把命令的用法打印到控制台
    flag.Parse()

    args := flag.Args()
    if len(args) > 0 {
        cmd.class = args[0]
        cmd.args = args[1:]
    }

    return cmd
}

func printUsage(){
    fmt.Printf("Usage: %s [-options] class [args...]")
}

在Java語言中,API一般以類庫的形式提供.在Go語言中,API則是以包(package)的形式提供. 

包可以向用戶提供常量,變量,結構體以及函數等.Java內置了豐富的類庫,Go同樣也內置了功能強大的包(package).

注意,與cmd.go文件一樣,main.go文件的包名也是main. 在Go語言中,main是一個特殊的包,這個包所在的目錄(可以叫做任何名字)會被編譯為可執行文件.

Go程序的入口也是main()函數,但是不接收任何參數,也不能有返回值.

go中,main()函數先調用ParseCommand()函數解析命令行參數,如果一切OK,則調用startJVM()函數啟動Java虛擬機.

如果解析出現錯誤,或者用戶輸入了-help選項,則調用PrintUsage()函數打印幫助信息.如果輸入-version.......

到這里,只是進行了模式.

在使用教程時,看到作者的github上的這個庫已經更新了.代碼跟這剛開始的差別很大.而且import "os"會在vscode中報錯.

 

> 第二章 搜索class文件

首先看一段親切的代碼:

加載HelloWorld類之前,首先要加載它的超類,也就是java.lang.Object . 在調用main()方法之前,因為虛擬機需要准備好參數數組,所以需要加載

java.lang.String和java.lang.String[]類. 把字符串打印到控制台還需要加載java.lang.System類,等等.那么Java虛擬機從哪里 尋找這些類呢?

Java虛擬機規范並沒有規定虛擬機應該從哪里尋找類,因此不同的虛擬機實現可以采用不同的方法. Oracle的Java虛擬機實現根據類路徑(class path)來搜索類.

按照搜索的先后順序, 類路徑可以分為下面3個部分:

- 1. 啟動類路徑 (bootstrap classpath)

- 2. 擴展類路徑 (extension classpath)

- 3. 用戶類路徑 (user classpath)

啟動類路徑默認對應jre\lib目錄,Java標准庫(大部分在rt.jar里)位於該路徑.

擴展類路徑默認對應jre\lib\ext目錄,使用Java擴展機制的類位於這個路徑.

我們自己實現的類,以及第三方類庫則位於用戶類路徑.

可以通過-Xbootclasspath選項修改啟動類路徑,不過通常並不需要這樣做.

用戶類路徑的默認值是當前目錄,也就是"." 可以設置CLASSPATH環境變量來修改用戶類路徑,但是這不夠靈活.

更好的辦法:給java命令傳遞-classpath(或簡寫為-cp)選項. -classpath/-cp選項的優先級更高,可以覆蓋CLASSPATH環境變量設置.

-classpath或-cp選項既可以知道目錄,也可以指定JAR文件或者ZIP文件.

java -cp path\to\classes

java -cp path\to\lib1.jar

java -cp path\to\lib2.zip

還可以同時指定多個目錄或文件,用分隔符分開即可. 分隔符因操作系統而異.

在Windows系統下是分號,在類UNIX(包括Linux,Mac OS X等)系統下是冒號.可以使用通配符 *

 

第二章的代碼建立在第一章的代碼基礎上.

Java虛擬機將使用JDK的啟動類路徑來尋找和加載Java標准庫中的類,因此需要某種方式指定jre目錄的位置.

命令行不錯,所以增加一個非標選項 -Xjre 

classpath文件夾新建Entry.go :

package classpath

import "os"
import "strings"

//常量,存放路徑分隔符
const pathListSeparator = string(os.pathListSeparator)

type Entry interface{
    //負責尋找和加載class文件;
    readClass(className string)([]byte, Entry,error)
    //該String()方法作用相當於java中的toString()用於返回變量的字符串表示
    String() string
}

func newEntry(path string) Entry {...}

readClass()方法的參數是class文件的相對路徑,路徑之間用斜線(/)分隔,文件名有.class后綴.

比如要讀取java.lang.Object類,傳入的參數應該是java/lang/Object.class

返回值是讀取到的字節數據,最終定位到class文件的Entry.以及錯誤信息.

Go的函數或方法允許返回多個值. 按照慣例. 可以使用最后一個返回值做為錯誤信息.

newEntry()函數根據參數創建不同類型的Entry實例.

具體Entry.go :

package classpath

import "os"
import "strings"

//常量,存放路徑分隔符
const pathListSeparator = string(os.pathListSeparator)

type Entry interface{
    //負責尋找和加載class文件;
    readClass(className string)([]byte, Entry,error)
    //該String()方法作用相當於java中的toString()用於返回變量的字符串表示
    String() string
}

//Entry接口有四個實現,分別是DirEntry,ZipEntry,CompositeEntry和WildcardEntry.
func newEntry(path string) Entry {
    if strings.Contais(path,pathListSeparator){
        return newCompositeEntry(path)
    }
    if strings.HasSuffix(path,"*"){
        return newWildcardEntry(path)
    }
    if strings.HasSuffix(path,".jar") || strings.HasSuffix(path,".JAR") ||
       stirngs.HasSuffix(path,".zip") || strings.HasSuffix(path,".ZIP"){
           return newZipEntry(path)
       }
    return newDirEntry(path)
}

上面os依舊報錯了

 

在上面說的四種實現中,DirEntry相對簡單一些,表示目錄形式的類路徑.

在ch02\classpath目錄下創建entry_dir.go文件,在其中定義DirEntry結構體:

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
    absDir string
}

func newDirEntry(path string) *DirEntry {}

func (self *DirEntry) readClass(className string)([]byte,Entry,error){}

func (self *DirEntry) String() string {}

newDirEntry() 先把參數轉換成絕對路徑,如果轉換過程出現錯誤, 則調用panic()函數終止程序執行.

否則創建DirEntry實例並返回.

完整的entry_dir.go :

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
    absDir string
}

func newDirEntry(path string) *DirEntry {
    absDir,err := filepath.Abs(path)
    if err != nil {
        panic(err)
    }
    return &DirEntry{absDir}
}

func (self *DirEntry) readClass(className string)([]byte,Entry,error){
    fileName := filepath.Join(self.absDir,className)
    data,err := ioutil.ReadFile(fileName)
    return data,self,err
}

//返回目錄
func (self *DirEntry) String() string {
    return self.absDir
}

 之后創建entry_zip.go

package classpath

import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"

type ZipEntry struct {
    absPath string //absPath字段存放ZIP或JAR文件的絕對路徑
}

func new ZipEntry(path string) *ZipEntry {
    absPath,err := filepath,Abs(path)
    if err != nil{
        panic(err)
    }
    return &ZipEntry{absPath}
}

func (self *ZipEntry) String() string {
    return self.absPath
}

//從ZIP文件中提取class文件:
func (self *ZipEntry) readClass(className string)([]byte,Entry,error){
    r,err := zip.OpenReader(self.absPath)
    if err != nil {
        return nil, nil, err
    }

    defer r.Close()
    for _, f := range r.File {
        if f.Name == className {
            rc,err := f.Open()
            if err != nil {
                return nil, nil, err
            }
        }
        defer rc.Close()
        data,err := ioutil.ReadAll(rc)
        if err != nil {
            return nil, nil, err
        }
        return data, self, nil
    }

    return nil, nil, errors.New("class not found: " + className)
}

該代碼首先打開ZIP文件, 如果這一步出錯的話,直接返回. 然后遍歷ZIP壓縮包里的文件, 看能否找到class文件.

如果能找到,則打開class文件,把內容讀取出來,並返回.

如果找不到,或者出現其它錯誤,則返回錯誤信息.有兩處使用了defer語句來確保打開的文件得以關閉.

readClass()方法每次都要打開和關閉ZIP文件,因此效率不是很高.

CompositeEntry由更小的Entry組成,正好可以表示成[]Entry. 在Go語言中,數組屬於比較低層的數據結構,很少直接使用.大部分情況下,

使用更便利的slice類型. 構造函數把參數(路徑列表)按分隔符分成小路徑. 然后把每個小路經都轉換成具體的Entry實例.

 

package classpath

import (
    "errors"
    "strings"
)

type CompositeEntry struct {
    entries []Entry
}

func newCompositeEntry(pathList string) *CompositeEntry {
    compoundEntry := &CompositeEntry{}

    for _, path := range strings.Split(pathList, pathListSeparator) {
        entry := newEntry(path)
        compoundEntry.addEntry(entry)
    }

    return compoundEntry
}

func (self *CompositeEntry) addEntry(entry Entry) {
    self.entries = append(self.entries, entry)
}

func (self *CompositeEntry) readClass(className string) (Entry, []byte, error) {
    for _, entry := range self.entries {
        entry, data, err := entry.readClass(className)
        if err == nil {
            return entry, data, nil
        }
    }

    return self, nil, errors.New("class not found: " + className)
}

func (self *CompositeEntry) String() string {
    strs := make([]string, len(self.entries))

    for i, entry := range self.entries {
        strs[i] = entry.String()
    }

    return strings.Join(strs, pathListSeparator)
}

以上為完整版.

這里應該明白了一件事,DirEntry也好,ZipEntry,CompositeEntry,WildcardEntry也好,可以讀取被編譯好的java庫的class封裝包(jar或其它格式).

這個readClass()方法,依次調用每一個路徑的readClass()方法,如果成功讀取到class數據,返回數據即可.

如果收到錯誤信息,則繼續;如果遍歷完所有的子路徑還沒有找到class文件,則返回錯誤.

String()方法也不復雜,調用每一個子路徑的String()方法,然后把得到的字符串用路徑分隔符拼接起來即可.

接下來是WildcardEntry:

WildcardEntry實際上也是CompositeEntry, 所以就不再定義新的類型了. 

package classpath

import (
    "os"
    "path/filepath"
    "strings"
)

type WildcardEntry struct {
    CompositeEntry
}

func newWildcardEntry(path string) *WildcardEntry {
    baseDir := path[:len(path)-1] // remove *
    entry := &WildcardEntry{}

    walkFn := func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if info.IsDir() && path != baseDir {
            return filepath.SkipDir
        }
        if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
            jarEntry := newZipEntry(path)
            entry.addEntry(jarEntry)
        }
        return nil
    }

    filepath.Walk(baseDir, walkFn)

    return entry
}

首先newWildcardEntry這個構造器方法把傳入路徑的星號去掉,得到baseDir,然后調用filepath的包的Walk()函數遍歷baseDir創建ZipEntry.

Walk()函數的第二個參數也是一個函數,了解函數式編程的讀者應該一眼就可以認出這個用法(即函數可做為參數) . 

walkFn中,根據后綴名選出JAR文件,並且返回SkipDir跳過子目錄(通配符類路徑不能遞歸匹配子目錄下的JAR文件).

接下來是classpath結構體.

package classpath

import (
    "path/filepath"
    "strings"

    "github.com/zxh0/jvm.go/jvmgo/options"
)

type ClassPath struct {
    CompositeEntry
}

func Parse(cpOption string) *ClassPath {
    cp := &ClassPath{}
    cp.parseBootAndExtClassPath()
    cp.parseUserClassPath(cpOption)
    return cp
}

func (self *ClassPath) parseBootAndExtClassPath() {
    // jre/lib/*
    jreLibPath := filepath.Join(options.AbsJavaHome, "lib", "*")
    self.addEntry(newWildcardEntry(jreLibPath))

    // jre/lib/ext/*
    jreExtPath := filepath.Join(options.AbsJavaHome, "lib", "ext", "*")
    self.addEntry(newWildcardEntry(jreExtPath))
}

func (self *ClassPath) parseUserClassPath(cpOption string) {
    if cpOption == "" {
        cpOption = "."
    }
    self.addEntry(newEntry(cpOption))
}

// className: fully/qualified/ClassName
func (self *ClassPath) ReadClass(className string) (Entry, []byte, error) {
    className = className + ".class"
    return self.readClass(className)
}

func (self *ClassPath) String() string {
    userClassPath := self.CompositeEntry.entries[2]
    return userClassPath.String()
}

func IsBootClassPath(entry Entry) bool {
    if entry == nil {
        // todo
        return true
    }

    return strings.HasPrefix(entry.String(), options.AbsJreLib)
}

上面的路徑還未加入.

getJreDir()函數優先使用用戶輸入的-Xjre選項作為jre目錄. 如果沒有輸入該選項,則在當前目錄下尋找jre目錄,如果找不到,嘗試使用JAVA_HOME環境變量.

exists()函數用於判斷目錄是否存在.

如果用戶沒有提供-classpath/-cp選項,則使用當前目錄作為用戶類路徑.

ReadClass()方法依次從啟動類路徑,擴展類路徑和用戶類路徑中搜索class文件,代碼.注意,傳遞給

ReadClass()方法的類名不包含".class"后綴. 最后, String()方法返回用戶類路徑的字符串表示.

其實在更新后的代碼不太准確.稍后將初始版的ch01和ch02上傳到github供后來人參考.

 

待上傳


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM