Bootstrap

动手实现自己的 JVM——Go!(ch02)

动手实现自己的 JVM——Go!(ch02)

参考张秀宏老师的《自己动手写java虚拟机》

ch02代码结构

image-20250213133952673

一、启动 Java 程序的过程

启动 Java 程序的过程大致可以分为以下几个阶段:

  1. 启动 JVM
    当我们启动一个 Java 程序时,首先是启动 Java 虚拟机(JVM)。JVM 是运行 Java 程序的环境,它负责管理内存、加载类文件、执行字节码等任务。
  2. 加载主类
    启动 JVM 后,接下来 JVM 会根据我们指定的主类(通常是通过命令行参数指定)加载该类。JVM 会查找主类的字节码文件(通常是 .class 文件),并将其加载到内存中。
  3. 调用主类的 main 方法
    主类加载完成后,JVM 会自动调用主类的 main 方法作为程序的入口点,开始执行程序中的逻辑。
处理类加载的顺序

在加载我们自己的类之前,JVM 必须先加载其超类,确保类的继承结构能够正确解析和使用。例如,如果我们自己定义的类是继承自 java.lang.Object 的,JVM 必须先加载 Object 类。

此外,在调用主类的 main 方法之前,JVM 还需要加载一些基础类,如 java.lang.String。这是因为 main 方法的签名通常为 public static void main(String[] args),其中 String 类型的数组 args 就需要先加载 String 类。

本节的目标

本节的核心目标是了解如何找到这些类并进行加载。我们不仅要加载我们的应用程序类,还需要确保基础类(如 ObjectString 等)在执行时已被正确加载。为了实现这一点,我们需要解决的问题是:

二、类路径分类

根据类路径的不同来源和作用,类路径可以分为以下几种类型。按照搜索顺序,类路径的分类如下图所示:

类路径分类
启动类(Bootstrap ClassLoader)

启动类路径(Bootstrap ClassPath)主要包括 Java 核心类库,包含了 JVM 启动所需的基础类,如 java.lang.* 包中的类。启动类一般位于 Java 安装目录的 jre/lib 目录下。它包含了 Java 的核心类库,JVM 在启动时首先加载这些类,以确保 Java 程序能够正确运行。

  • 一般位于: jre/lib/rt.jar 或类似路径。

    image-20250213133521328
扩展类(Extension ClassLoader)

扩展类路径(Extension ClassPath)包含了 JDK 中的一些扩展库,这些库可以通过 -Djava.ext.dirs 来指定。扩展类一般包含了对 Java 核心功能的扩展,如 javax.*org.* 等包。

  • 一般位于: jre/lib/ext 目录。
用户类(Application ClassLoader)

用户类路径(Application ClassPath)是我们在编写 Java 程序时,直接引用的类文件所在的路径。它包含了用户编写的程序的 .class 文件及其他依赖的第三方库(如 JAR 文件)。在程序启动时,JVM 会按照指定的类路径(通过 -cp-classpath 参数设置)来查找用户自定义的类。

  • 一般位于: 用户指定的路径,通常是应用程序的 bin 目录或外部 JAR 文件。

修改 Cmd

首先,在第一章的基础上,我们需要修改 Cmd 类以支持新的命令行选项。具体的修改如下:

type Cmd struct {
	helpFlag    bool
	versionFlag bool
	cpOption    string
	XjreOption  string  // 新增的选项
	class       string
	args        []string
}

func parseCmd() *Cmd {
	cmd := &Cmd{}
	flag.Usage = printUsage
	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")
	flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre") // 新增的选项,用于指定 JRE 路径
	flag.Parse()

	// 继续处理命令行参数...
}

在这个修改中,主要增加了一个新的选项 -Xjre,用于指定 JRE 路径。这一修改使得命令行能够接收更多的参数,便于启动 Java 程序时指定不同的 JRE 路径。

三、具体编码实现

(1)类路径项的视线

设计模式——组合模式:接口实现与示例

在张宏秀老师的代码中,采用了组合模式来设计类路径的结构。组合模式通过将对象组合成树形结构,使得客户端可以一致地对待单个对象和组合对象。在 Go 语言中,组合模式的实现通常通过接口和结构体的组合来完成。

1. 接口定义:Entry

首先,我们定义了一个接口 Entry,它规定了两个方法:

  • readClass(className string) ([]byte, Entry, error):用来读取指定类的字节内容。
  • String():返回 Entry 对象的字符串表示。
type Entry interface {
	readClass(className string) ([]byte, Entry, error)
	String() string
}

接口是 Go 中面向对象编程的核心,任何类型只要实现了接口中的所有方法,就自然地成为该接口的实现类型。Go 语言没有显式的 implements 关键字,而是通过“结构体方法集”来自动匹配接口。

2. 接口实现:不同类型的 Entry

根据类路径的不同类型(如普通目录、ZIP 文件、通配符路径等),我们提供了不同的 Entry 实现,以下是主要实现的概述。

newEntry 函数:根据路径创建合适的 Entry 类型
func newEntry(path string) Entry {
	// 分隔符用于分隔路径,适应操作系统的不同
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)  // 处理多个路径组成的复合路径
	}
	if strings.HasSuffix(path, "*") {
		return newWildcardEntry(path)  // 处理通配符路径
	}
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
		return newZipEntry(path)  // 处理 JAR 或 ZIP 文件
	}
	return newDirEntry(path)  // 处理普通目录
}

newEntry 函数中,我们根据不同的路径形式,返回不同的 Entry 实现:

  • newCompositeEntry(path):用于处理多个路径组合的情况,可能会是一个包含多个路径的复合路径。
  • newWildcardEntry(path):用于处理通配符路径(如 *.jar),这通常表示可以匹配多个文件。
  • newZipEntry(path):用于处理 JAR 或 ZIP 文件路径。
  • newDirEntry(path):用于处理普通的目录路径。

每个 Entry 类型(如 ZipEntryDirEntry 等)都会实现 Entry 接口中的方法(readClassString),从而形成了一个灵活的、可以扩展的设计。

3. 示例:如何在 Go 中实现接口

下面,我们以 ZipEntry 作为示例,展示如何在 Go 中实现接口。

ZipEntry 实现 Entry 接口
package classpath

import (
	"archive/zip"
	"fmt"
	"io"
	"path/filepath"
)

type ZipEntry struct {
	absPath string
}

func newZipEntry(path string) *ZipEntry {
	absPath, err := filepath.Abs(path)
	fmt.Printf("absPath:%v\n", absPath)
	if err != nil {
		panic(err)
	}
	return &ZipEntry{absPath}
}
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
	fmt.Println("调用了zip")
	fmt.Printf("self.absPath=%s\n", self.absPath)

	// 打开zip文件
	r, err := zip.OpenReader(self.absPath)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to open zip file: %v", err)
	}
	defer r.Close()

	// 遍历压缩包中的文件
	for _, file := range r.File {
		if file.Name == className {
			rc, err := file.Open()
			if err != nil {
				return nil, nil, fmt.Errorf("failed to open class file: %v", err)
			}
			defer rc.Close()

			// 读取文件内容
			data, err := io.ReadAll(rc)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to read class file: %v", err)
			}

			// 返回文件内容、当前 entry 和 nil 错误
			return data, self, nil
		}
	}

	// 如果没有找到class文件,返回错误
	return nil, nil, fmt.Errorf("class %s not found in zip", className)
}

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

在这个例子中:

  • ZipEntry 结构体实现了 Entry 接口。
  • readClass 方法尝试从 ZIP 文件中读取指定的类文件内容。
  • String 方法返回该 ZipEntry 的路径信息。
4. 接口实现总结

Go 语言的接口实现是隐式的,这意味着我们不需要显式地声明一个类型实现了某个接口,只要该类型实现了接口中的所有方法,Go 会自动认为它实现了该接口。

Go 接口实现的特点:
  • 隐式实现:无需显式声明类型实现了接口,接口方法集符合即可。
  • 结构体与接口解耦:结构体不需要继承接口,而是通过方法集的匹配实现接口。
  • 灵活性和扩展性:可以非常灵活地为新的类型提供接口实现,符合“面向接口编程”的思想。

组合模式通过接口和结构体的组合,实现了不同路径类型的处理。例如,通过 Entry 接口,我们统一管理了不同类型路径的读取逻辑(如文件路径、JAR 文件、目录等)。这种方式增强了代码的可扩展性和维护性。

当添加新的路径类型时,我们只需要实现 Entry 接口,而不需要修改现有的代码结构。这使得系统更加灵活且具有较高的可扩展性。

通过这样的设计,程序能够处理不同类型的类路径(包括普通文件、JAR 文件、目录、通配符等),并且对于客户端代码来说,无论是单个文件还是复合路径,它们的处理方式是统一的。

ps:如果需要总的代码,可以去我的github或者张宏秀老师的github扒拉哦:9lucifer/go_jvm: 根据张宏秀老师的著作编写的分章节jvm

(2)classpath类的实现
package classpath

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

type ClassPath struct {
	bootClasspath Entry
	extClasspath  Entry
	userClasspath Entry
}

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

func (self *ClassPath) parseBootAndExtClassPath(jreOption string) {
	jreDir := getJreDir(jreOption)
	// jre/lib/*
	jreLibPath := filepath.Join(jreDir, "lib", "*")
	self.bootClasspath = newWildcardEntry(jreLibPath)

	// jre/lib/ext/*
	jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")

	self.extClasspath = newWildcardEntry(jreExtPath)
}

func getJreDir(jreOption string) string {
	if jreOption != "" && exists(jreOption) {
		return jreOption
	}
	if exists("./jre") {
		return "./jre"
	}
	if jh := os.Getenv("JAVA_HOME"); jh != "" {
		return filepath.Join(jh, "jre")
	}
	panic("can not find jre dir")
}

func exists(path string) bool {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

func (self *ClassPath) String() string {
	return self.userClasspath.String()
}

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

	// 1. 先检查 bootClasspath
	data, entry, err := self.bootClasspath.readClass(className)
	if err != nil {
		fmt.Printf("在 bootClasspath 中未找到类 %s: %v\n", className, err)
	} else {
		return data, entry, nil // 如果找到类,直接返回
	}

	// 2. 如果 bootClasspath 中没有找到,继续检查 extClasspath
	data, entry, err = self.extClasspath.readClass(className)
	if err != nil {
		fmt.Printf("在 extClasspath 中未找到类 %s: %v\n", className, err)
	} else {
		return data, entry, nil // 如果找到类,直接返回
	}

	// 3. 如果 extClasspath 中也没有找到,最后检查 userClasspath
	data, entry, err = self.userClasspath.readClass(className)
	if err != nil {
		fmt.Printf("在 userClasspath 中未找到类 %s: %v\n", className, err)
		return nil, nil, fmt.Errorf("类 %s 未找到", className)
	}

	// 如果找到类,返回结果
	return data, entry, nil
}

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

代码解析

这段代码实现了类路径(ClassPath)的解析和类加载功能,它的作用是帮助程序确定和加载 Java 类文件。具体来说,代码定义了一个 ClassPath 结构体,包含了三个不同的类路径(bootClasspath、extClasspath、userClasspath),并根据提供的路径来加载类文件。

  1. ClassPath 结构体:
    • bootClasspath: 启动类路径,用于加载 JDK 自带的核心类库。
    • extClasspath: 扩展类路径,用于加载 JDK 的扩展库。
    • userClasspath: 用户类路径,用于加载用户自己编写的类。
  2. Parse 函数:
    • 这是 ClassPath 的构造函数,用于解析启动类路径和用户类路径。它调用了 parseBootAndExtClassPathparseUserClassPath 来设置类路径。
  3. parseBootAndExtClassPath 函数:
    • 这个函数根据 JRE 目录的路径(jreOption)来确定 boot 和 ext 类路径。
    • jreDir 是 JRE 的目录,如果指定了 JRE 路径则使用指定的路径,否则尝试使用默认路径。
    • bootClasspathextClasspath 分别指向 JRE 中的 liblib/ext 目录,用于加载 Java 的核心类和扩展类。
  4. getJreDir 函数:
    • 用来确定 JRE 目录的位置。首先会检查 jreOption 路径是否存在,其次检查当前目录下是否有 ./jre 目录,最后从环境变量 JAVA_HOME 获取路径。
  5. exists 函数:
    • 检查一个路径是否存在。通过 os.Stat 来判断文件或目录是否存在。
  6. ReadClass 函数:
    • 这个函数用于读取一个类的字节数据,按照以下顺序检查类路径:
      1. 首先在 bootClasspath 中查找。
      2. 如果没找到,则在 extClasspath 中查找。
      3. 如果依然没找到,最后检查 userClasspath
    • 如果在某个路径中找到了类,返回类的字节数据;如果所有路径都未找到,返回错误。
  7. String 函数:
    • 返回用户类路径的字符串表示,通常用于调试输出。
  8. parseUserClassPath 函数:
    • 用于解析用户类路径。如果没有指定用户类路径,则默认为当前目录 .
(3)测试类——在之前的基础上修改

为了观感,我做了转16进制的处理

func startJvm(cmd *Cmd) {
	fmt.Printf("cmd.class : %v\n", cmd.class)
	cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
	fmt.Printf("classpath: %v\n class:%v\n args:%v\n", cp, cmd.class, cmd.args)
	className := strings.Replace(cmd.class, ".", "/", -1)

	fmt.Printf("className:%v\n", className)
	classDate, _, err := cp.ReadClass(className)
	if err != nil {
		// 在这里出错了
		fmt.Printf("could not find the class \"%s\"\n", cmd.class)
		return
	}

	// 将 classDate 转换为十六进制字符串
	hexData := fmt.Sprintf("%x", classDate)

	// 格式化输出:每行 8 个字节,每个字节之间用空格分隔
	prettyHex := ""
	for i := 0; i < len(hexData); i += 2 {
		if i > 0 {
			if i%64 == 0 { // 每 8 个字节换行
				prettyHex += "\n"
			} else {
				prettyHex += " "
			}
		}
		prettyHex += hexData[i : i+2]
	}

	fmt.Printf("classmame:%v \nclassDate is:\n%v\n", className, prettyHex)
}

四、测试代码

如果没有一次性成功运行,完全正常!我自己也遇到过很多次失败(哭)😭

image-20250213134705895

效果——能够看到 cafebabe,成功了!🎉

image-20250213134751988

;