动手写java虚拟机_[JVM] - 一份<自己动手写Java虚拟机>的测试版

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

参照

> 第一章 指令集和解释器

5f68936ebbf65a5dc6f7a542ac940491.png

生成了ch01.exe文件

256c95807fdfd71affdf3ab4ce75485d.png

这里还生成了一个gopkgs.exe文件

fad680077c00ba7a805997d6ea3f3901.png

执行以上操作,这里说明: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 versionandexit")

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文件

首先看一段亲切的代码:

027bdacf387a534d75b90c6657577acd.png

加载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等)系统下是冒号.可以使用通配符 *

第二章的代码建立在第一章的代码基础上.

7eadd81c4d1fef62a3d5774ed73a1689.png

Java虚拟机将使用JDK的启动类路径来寻找和加载Java标准库中的类,因此需要某种方式指定jre目录的位置.

命令行不错,所以增加一个非标选项 -Xjre

20b243b2646482d638fd44a88994d755.png

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)}

ifstrings.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}

returnnil, 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)}returnnil}

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供后来人参考.

894e1569b30657f6a26418b11e60e4eb.png

待上传

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值