侧边栏壁纸
博主头像
LYMTICS

海纳百川,有容乃大

  • 累计撰写 45 篇文章
  • 累计创建 37 个标签
  • 累计收到 19 条评论

目 录CONTENT

文章目录

自己动手写JVM(二)

LYMTICS
2022-01-27 / 1 评论 / 2 点赞 / 122 阅读 / 10,243 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-01-27,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

自己动手写JVM(二)

学习《自己动手写Java虚拟机·张秀宏》自己记的笔记。

简介

这一张完成的任务是:找包。

通俗来说就是去哪里找到我们要去处理的那个包的位置,是 JDK 自带的包还是用户自己写的包等等。

我们知道 JVM 采用了双亲委派机制寻找包,所以应该会按照一定的顺序去找包:

  1. 启动类路径
  2. 扩展类路径
  3. 用户类路径

前两者和 JDK 安装的位置有关,我们只需按 JAVA_HOME 处理就好。

而用户类路径则是一个难点:

  1. 默认是当前路径,且可以用环境变量 CLASSPATH-cp/classpath 参数(优先级递增)指定。
  2. 不仅可以指定为路径名,还可以指定为 JAR 文件或 ZIP 文件
  3. 可以同时指定多个用分隔符分开的目录或文件,也可以用通配符 * 指定所有的 JAR 文件

系统分隔符:

  • windows:;
  • linux::

新增的结构

Entry

Entry 是一个表示classpath的接口,下面有许多实现分别处理上面提到的不同类型的 classpath。

有两个方法:

  1. readClass(className string) 在这个classpath下读取className类
  2. String 返回绝对路径的字符串表示

至于应该创建哪个类的实例,是由一个函数决定的:

// 按照具体path创建相应的实现类: 
// DirEntry | ZipEntry | CompositeEntry | WildcardEntry
func newEntry(path string) Entry {
	// 有 : 或 ; 说明不止一个
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)
	}
	// 有通配符 * 说明不止一个
	if strings.HasSuffix(path, "*") {
		return newWildcardEntry(path)
	}
	// 压缩包 ZIP 或 JAR
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
		return newZipEntry(path)
	}
	// 否则,是最基础的路径
	return newDirEntry(path)
}

Entry 的四个分类:

  1. DirEntry:是 Entry 的子类,处理类似/codingspace/javaproject/的路径
  2. ZipEntry:是 Entry 的子类,处理压缩包,如 /xxx/xxx/xx.jar/xxx/xx.zip
  3. CompositeEntry :是 Entry 数组,分别处理多个由分隔符分隔开的路径

注意点:

  1. 注意:这里 Entry 只指向一个路径或压缩包,readClass才是解决具体是哪个class文件,不要搞混了

  2. filepath.Abs(XXX) 是相对于当前路径的绝对路径,也即初始化时输入的路径是在当前路径继续往下找的

    image

ClassPath

  • 解决了三种不同类路径的先后关系
  • 是第一章中 Cmd 和本章中 Entry 的桥梁,对 Cmd 中的参数进行处理,以及调用 Entry

另外,有一个问题需要大家注意:

JDK的目录随着发展也在不断变化之中,所以我们应该用笔者用的版本才能正确执行程序。另外,由于自己经常需要用到JDK,所以也不方便把JDK版本直接降下来,所以就在环境变量中用 GVM_HOME 变量代替 JAVA_HOME 了。

代码及注释

为了便于测试,笔者增加了一些日志输出,便于观察学习

entry.go

package classpath

import (
	"os"      // 用来获取系统分隔符
	"strings" // 字符串相关的工具方法
)

// 获取系统分隔符
const pathListSeparator = string(os.PathListSeparator)

// 声明了一个接口
type Entry interface {
	// 寻找和加载class文件
	readClass(className string) ([]byte, Entry, error)
	// 返回变量的字符串表示
	String() string
}

// 按照具体path创建相应的实现类: DirEntry | ZipEntry | CompositeEntry | WildcardEntry
func newEntry(path string) Entry {
	// 有 : 或 ; 说明不止一个
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)
	}
	// 有通配符 * 说明不止一个
	if strings.HasSuffix(path, "*") {
		return newWildcardEntry(path)
	}
	// 压缩包 ZIP 或 JAR
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
		return newZipEntry(path)
	}
	// 否则,返回最基础的
	return newDirEntry(path)
}

entry_dir.go

package classpath

// 创建一个Entry的实现类: DirEntry 用来解决一个类的情况

import (
	"io/ioutil" // 读取内容 input ouput util
	"log"
	"path/filepath" // 将相对路径转换为绝对路径
)

// 作者: 不需要显式implements, 只要实现相关方法就可以
type DirEntry struct {
	absDir string // 保存绝对路径
}

// 相当于构造函数, 以new开头, 是本书作者的风格
func newDirEntry(path string) *DirEntry {
	// 将相对路径转换为绝对路径
	absDir, err := filepath.Abs(path)
	if err != nil {
		panic(err) // 中断这个任务
	}
	// 好神奇, 这里没有指明具体赋值给哪个变量(这里说'属性'似乎不合适)
	// 这是因为只有一个变量还是因为名称对应(Go的特性,名称对应可以直接赋值)
	return &DirEntry{absDir}
}

// 将指定路径的class文件以字节方式加载入内存中
// 感觉go这个编程风格挺好的, C++需要把函数声明放在头文件里反而有些冗余
// 这是先确定目录, 再确定class文件啊
func (dirEntry *DirEntry) readClass(className string) ([]byte, Entry, error) {
	fileName := filepath.Join(dirEntry.absDir, className)
	log.Println("entry_dir.go:readClass: 最终定位的Class文件为: ", fileName)
	data, err := ioutil.ReadFile(fileName)
	return data, dirEntry, err
}

// 返回绝对路径的字符串
func (dirEntry *DirEntry) String() string {
	return dirEntry.absDir
}

entry_zip.go

zipjar 与前面的主要区别就是这俩的最后一步不能用路径指定了,得去压缩包里找

package classpath

import (
	"archive/zip" // 读取压缩包
	"errors"
	"io/ioutil"
	"path/filepath" // 获取绝对路径
)

type ZipEntry struct {
	absPath string // 绝对路径
}

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

// 读取zip文件
func (zipEntry *ZipEntry) readClass(className string) ([]byte, Entry, error) {
	r, err := zip.OpenReader(zipEntry.absPath)
	if err != nil {
		return nil, nil, err
	}
	// defer: 相当于finally, 当函数执行完后执行
	// 先defer的后执行(相当于栈)
	defer r.Close()

	// 对于压缩包中的文件, 寻找那个指定的class
	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, zipEntry, nil
		}
	}
	return nil, nil, errors.New("class not found: " + className)
}

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

entry_composite.go

package classpath

import (
	"errors"
	"strings"
)

// 定义结构是一个Entry数组
type CompositeEntry []Entry

func newCompositeEntry(pathList string) CompositeEntry {
	compositeEntry := []Entry{}

	for _, path := range strings.Split(pathList, pathListSeparator) {
		// 分别对每一个路径文件进行处理,封装在Entry中
		entry := newEntry(path)
		compositeEntry = append(compositeEntry, entry)
	}
	return compositeEntry
}

func (compositeEntry CompositeEntry) readClass(className string) ([]byte, Entry, error) {
	for _, entry := range compositeEntry {
		data, from, err := entry.readClass(className)
		if err == nil {
			return data, from, nil
		}
	}
	return nil, nil, errors.New("class not found: " + className)
}

func (compositeEntry CompositeEntry) String() string {
	strs := make([]string, len(compositeEntry))

	for i, entry := range compositeEntry {
		strs[i] = entry.String()
	}
	return strings.Join(strs, pathListSeparator)
}

entry_wildcard.go

首先要介绍两个函数:

func Walk(root string, fn WalkFunc) error

Walk walks the file tree rooted at root, calling fn for each file or directory in the tree, including root. Document - filepath.Walk

即:对路径root下(包括root)的每个文件执行fn方法(WalkFunc是个类型,准确来说是一个函数,这里其实是函数式编程):

type WalkFunc Document

type WalkFunc func(path string, info fs.FileInfo, err error) error

文档里说的很清楚这两个函数是干什么的

package classpath

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

// 其实也是[]Entry, 所以就不定义新的类型了
func newWildcardEntry(path string) CompositeEntry {
	log.Println("entry_wildcard.go:newWildCardEntry: 构建WildcardEntry, 参数path: ", path)
	baseDir := path[:len(path)-1] // remove *
	compositeEntry := []Entry{}

	// 定义一个函数 walkFn, 作为参数传递给 filepath.Walk() 从而遍历目录
	walkFn := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		// 跳过文件夹和根目录(Walk会先遍历root)
		if info.IsDir() && path != baseDir {
			log.Println("entry_wildcard.go:newWildCardEntry:walkFn 跳过目录:  ", path)
			return filepath.SkipDir
		}
		if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
			log.Println("entry_wildcard.go:newWildCardEntry:walkFn 扫描到JAR包:  ", path)
			jarEntry := newZipEntry(path)
			compositeEntry = append(compositeEntry, jarEntry)
		}
		return nil
	}

	filepath.Walk(baseDir, walkFn)
	log.Println("entry_wildcard.go:newWildCardEntry: Walk执行完毕")

	log.Println("entry_wildcard.go:newWildCardEntry: 最终扫描到JAR包的数量为 ", len(compositeEntry))

	return compositeEntry
}

classpath.go

为了便于学习,修改了原文中的 String() 方法

package classpath

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

type Classpath struct {
	bootClasspath Entry // 启动类路径
	extClasspath  Entry // 扩展类路径
	userClasspath Entry // 用户类路径
}

// 根据jreOption来parse Classpath
func Parse(jreOption, cpOption string) *Classpath {
	log.Println("classpath.go:Parse: 开始渲染Classpath")
	cp := &Classpath{}
	// 前两种
	cp.parseBootAndExtClasspath(jreOption)
	// 用户类路径
	cp.parseUserClasspath(cpOption)
	return cp
}

// 具体的渲染启动类路径和扩展类路径的方法
func (classpPath *Classpath) parseBootAndExtClasspath(jreOption string) {
	// 获取jre路径
	log.Println("classpath.go:parseBootAndExtClasspath: 获取JRE路径")
	jreDir := getJreDir(jreOption)
	log.Println("classpath.go:parseBootAndExtClasspath: 最终JRE目录: ", jreDir)

	// jre/lib/*
	// 从上面获取的路径参数/lib/*下寻找
	jreLibPath := filepath.Join(jreDir, "lib", "*")
	log.Println("classpath.go:parseBootAndExtClasspath: BootstrapClasspath尝试的路径为: ", jreLibPath)
	classpPath.bootClasspath = newWildcardEntry(jreLibPath)

	// jre/lib/ext/*
	// 同上
	jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
	log.Println("classpath.go:parseBootAndExtClasspath: ExtensionClasspath尝试的路径为: ", jreExtPath)
	classpPath.extClasspath = newWildcardEntry(jreExtPath)
}

// 按照一定的优先级规则,获取JRE路径
func getJreDir(jreOption string) string {
	// 如果用户有指定,那就用用户指定的
	if jreOption != "" && exists(jreOption) {
		log.Println("classpath.go:getJreDir: 命令行JRE参数: ", jreOption)
		return jreOption
	}
	// 否则,如果当前目录存在./jre目录,就把这个路径返回
	if exists("./jre") {
		log.Println("classpath.go:getJreDir: 当前路径下存在./jre目录")
		return "./jre"
	}
	// 否则,如果环境变量中存在JAVA_HOME,就用这个路径
	if jh := os.Getenv("GVM_HOME"); jh != "" {
		log.Println("classpath.go:getJreDir: 使用JAVA_HOME下的JRE目录")
		return filepath.Join(jh, "jre")
	}
	// 否则,抛出一个异常
	panic("Can not find jre folder!")
}

// 判断当前路径是否存在指定路径 相对的是exe文件的路径
func exists(path string) bool {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

// 渲染用户类路径
func (classPath *Classpath) parseUserClasspath(cpOption string) {
	// 如果用户参数为空,变成'.'(默认当前路径)
	if cpOption == "" {
		cpOption = "."
		log.Println("classpath.go:parseUserClasspath: 命令行-cp参数为空,默认替换为当前目录")
	}
	log.Println("classpath.go:parseUserClassPath: cp最终取值为: ", cpOption)
	// Entry的构造函数会根据用户的输入是否有分隔符、后缀等条件判断创建具体的实例
	classPath.userClasspath = newEntry(cpOption)
}

// className: fully/qualified/ClassName
// 读取类字节流
// 按照双亲委派机制的顺序读取 不过我记得一本书里说JVM里是用递归实现的
func (classPath *Classpath) ReadClass(className string) ([]byte, Entry, error) {
	log.Println("classpath.go:ReadClass: 开始读取Class中的数据")
	className = className + ".class"
	log.Println("classpath.go:ReadClass: className: ", className)
	log.Println("classpath.go:ReadClass: 尝试从启动类路径获取")
	if data, entry, err := classPath.bootClasspath.readClass(className); err == nil {
		log.Println("classpath.go:ReadClass: 从启动类路径获得了数据")
		return data, entry, err
	}
	log.Println("classpath.go:ReadClass: 尝试从扩展类路径获取")
	if data, entry, err := classPath.extClasspath.readClass(className); err == nil {
		log.Println("classpath.go:ReadClass: 从扩展类路径获得了数据")
		return data, entry, err
	}
	log.Println("classpath.go:ReadClass: 尝试从用户类路径获取")
	return classPath.userClasspath.readClass(className)
}

func (classPath *Classpath) String() string {
	return "\nBootstrap: " + classPath.bootClasspath.String() +
		"\nExtension: " + classPath.extClasspath.String() +
		"\nUser: " + classPath.userClasspath.String()
}

main.go

func startJVM(cmd *Cmd) {
	// 渲染指定JRE路径和ClassPath路径的参数
	cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
	log.Println("main.go:startJVM: 处理完的类路径为: ")
	log.Println(cp)
	log.Println("main.go:startJVM: 用户输入的类为: ", cmd.class)
	log.Println("main.go:startJVM: 额外的参数: ", cmd.args)

	// 把类路径转换为路径
	className := strings.Replace(cmd.class, ".", "/", -1)
	// 读取字节流数据
	classData, _, err := cp.ReadClass(className)
	if err != nil {
		fmt.Printf("Could not find or load main class %s\n", cmd.class)
		return
	}

	// 输出
	fmt.Printf("class data:%v\n", classData)
}

测试

扫描classpath的过程:

看来基本符合我们预期的

image

由于日志的引入比较长,所以就不继续测试了,你可以试试如下情况:

  1. -cp 指定用户类路径,-Xjre 指定JRE路径
  2. 优先级问题
  3. 其他问题
2

评论区