写自己的JVM(0x1)

Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手写一个。但是这个系列的定位还是入门,所以有很多的东西暂时都不会实现,比如 malloc,GC,多线程,native interface等。主要的目的还是在于大致了解JVM是一个什么样的东西,这也是我采用java来实现JVM的一个原因,便于理解。

由于我之前也实现过一个简单的可以执行汇编的模拟器,写这个对我来说不难,但是可能出现有些地方我认为理所当然,而刚接触的读者很难理解的情况。如果出现了,欢迎大家在原博客中评论。

下面是原始博客的地址:

lyldalek.notion.site/JVM-0x1-c14...

该项目的地址:

github.com/aprz512/wri...

项目的完成度现在在90%左右了,运行一些简单的类是没有问题的,后面会慢慢扩展。

该系列的文章会对项目的代码进行详细的介绍。这个系列大致会分为下面几个部分:

  1. 实现 classpath,暂时只兼容 windows,有兴趣参与进来的可以提pr
  2. 实现 class 文件的解析
  3. 实现运行时数据区
  4. 实现解释器,解释 method 中的指令
  5. 实现类与对象的支持
  6. 实现方法调用
  7. 实现数组
  8. 实现异常

该项目并不会去实现对象分配与内存管理,有兴趣的可以看看 csapp 的 malloc lab。

项目的参考资料:

  • 《自己动手写Java虚拟机》,作者使用go语言实现。
  • 官方文档
  • 其他类似的开源项目等

虚拟机介绍

看上图,类加载子系统将 class 文件加载到内存中,这个过程需要完成2件事:

  • 找到 class 文件的路径
  • 将 class 文件按照格式解析

这篇文章,主要是完成第1件事,寻找 class 文件。

类路径

Java虚拟机规范并没有规定虚拟机应该从哪里寻找类,所以我们可以自由一点。但是回想一下,java 加载类的时候,它通过双亲委派机制来加载类,所以类路径至少有3个:

  • 启动类路径(bootstrap classpath),路径默认对应jre\lib目录
  • 扩展类路径(extension classpath),路径默认对应jre\lib\ext目录
  • 用户类路径(user classpath),路径的默认值是当前目录

在 windows 上,这些路径都可以通过 JAVA_HOME 拿到。

搜索类

有了类路径,我们就可以在这些路径下面搜索指定的类了。搜索会发生以下几种情况:

  • class 文件是一个单独的文件,在当前目录下
  • class 文件是一个单独的文件,在当前目录的子目录里面
  • class 文件在压缩文件里面

根据这些情况,我们使用组合模式来写代码,看以下组合模式的类图:

我们仿照这个类图,首先定义一个接口:

ruby 复制代码
public interface Entry {

    // className: fully/qualified/ClassName.class
    // for example: write/your/own/jvm/classpath/Entry.class
    byte[] readClass(String fullyQualifiedClassName) throws Exception;

}

这里的参数格式需要注意,是全限定类名的形式。

DirEntry

我们先实现最简单的搜索情况.

java 复制代码
public class DirEntry implements Entry {

    // directory absolute path which your target class file in
    // 在 Solaris OS 中,Path 使用 Solaris 语法(/home/joe/foo)
    // 在 Microsoft Windows 中,Path 使用 Windows 语法(C:\home\joe\foo)。
    private final Path absDirPath;

    public DirEntry(String classpath) {
        absDirPath = Paths.get(classpath).toAbsolutePath();
    }

    @Override
    public byte[] readClass(String fullyQualifiedClassName) throws Exception {
        return Files.readAllBytes(absDirPath.resolve(fullyQualifiedClassName));
    }
}

由于类文件是在当前路径下,所以我们直接读取文件流就好了。

ZipEntry

java 复制代码
public class ZipEntry implements Entry {

    // zip file's absolute path
    // 在 Solaris OS 中,Path 使用 Solaris 语法(/home/joe/foo)
    // 在 Microsoft Windows 中,Path 使用 Windows 语法(C:\home\joe\foo)。
    private final Path absZipFilePath;

    public ZipEntry(String classPath) {
        absZipFilePath = Paths.get(classPath).toAbsolutePath();
    }

    @Override
    public byte[] readClass(String fullyQualifiedClassName) throws Exception {
        // unzip jar/zip file
        // read class file bytes from zip file
        // This method makes use of specialized providers that create pseudo file systems
        // where the contents of one or more files is treated as a file system.
        // pass null to loader means only attempt to locate an installed provider
        try (FileSystem zipFs = FileSystems.newFileSystem(absZipFilePath, null)) {
            return Files.readAllBytes(zipFs.getPath(fullyQualifiedClassName));
        }
    }
}

读取当前目录下的 zip 文件里面的内容,稍微麻烦点,但是我们有新的api,所以代码也不多。

CompositeEntry

在程序的运行时,我们可以设置 -cp 参数,指定用户类路径,它可以利用; 来配置多个路径。

arduino 复制代码
public class CompositeEntry implements Entry {

    private final List<Entry> entries = new ArrayList<>();

    public CompositeEntry(String compositeClasspathList) {
        for (String classpath : compositeClasspathList.split(File.pathSeparator)) {
            entries.add(EntryFactory.create(classpath));
        }
    }

    @Override
    public byte[] readClass(String fullyQualifiedClassName) throws Exception {
        // search every entry to find class file
        for (Entry entry : entries) {
            try {
                return entry.readClass(fullyQualifiedClassName);
            } catch (Exception ignored) {
            }
        }
        // not found
        throw new Exception("class not found: " + fullyQualifiedClassName);
    }

}

整个逻辑简单,就是根据每个; 分割路径然后做一个解析,然后创建对应的 Entry:

arduino 复制代码
public class EntryFactory {

    public static Entry create(String classpath) {

        // /a/b/c ; /x/y/z.zip
        if (classpath.contains(File.pathSeparator)) {
            return new CompositeEntry(classpath);
        }

        // /a/b/*
        if (classpath.endsWith("*")) {
            return new WildcardEntry(classpath);
        }

        // /x/y/z.jar
        if (classpath.endsWith(".jar")
                || classpath.endsWith(".JAR")
                || classpath.endsWith(".zip")
                || classpath.endsWith(".ZIP")) {
            return new ZipEntry(classpath);
        }

        return new DirEntry(classpath);
    }

}

WildcardEntry

假如说我们需要加载 java/lang/String.class,该类在xxxx/Java/jdk-1.8/jre/lib的 rt.jar 里面:

但是我们最好不将类路径写死,所以可以采用通配符的方式。比如我想加载jre/lib 下的类,那么就可以指定为 jre/lib/*,这种写法会加载当前目录下所有 JAR文件。

typescript 复制代码
public class WildcardEntry extends CompositeEntry {

    public WildcardEntry(String wildcardPath) {
        super(compositePath(wildcardPath));
    }

    private static String compositePath(String wildcardPath) {
        // remove *
        String baseDir = wildcardPath.replace("*", "");
        try {
            // The try-with-resources Statement
            try (Stream<Path> pathStream = Files.walk(Paths.get(baseDir))) {
                // filter .jar and .JAR
                // collect
                return pathStream.filter(Files::isRegularFile)
                        .map(Path::toString)
                        .filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
                        .collect(Collectors.joining(File.pathSeparator));
            }
        } catch (IOException e) {
            //e.printStackTrace(System.err);
            return "";
        }
    }

}

实现也很简单,就是将 jre/lib 下的所有 jar 文件过滤出来,使用 ;拼接后就是一个 CompositeEntry 了。

Classpath

经过上面的步骤,我们类的搜索做完了,现在我们需要指定下面的三个路径:

  • 启动类路径(bootstrap classpath)
  • 扩展类路径(extension classpath)
  • 用户类路径(user classpath)

JRE路径

typescript 复制代码
  private static String getJreDir() {
        String jh = System.getenv("JAVA_HOME");
        if (jh != null) {
            return Paths.get(jh, "jre").toString();
        }
        throw new RuntimeException("Can not find JRE folder!");
    }

从系统设置的环境变量里面去读取。

启动类路径

arduino 复制代码
  private Entry parseBootStrapClasspath() {
        String jreDir = getJreDir();
        // jre/lib/*
        String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
        return new WildcardEntry(jreLibPath);
    }

注意看,我们这里添加了通配符,这样就可以遍历该目录下的所有 jar 文件。

扩展类路径

arduino 复制代码
  private Entry parseExtensionClasspath() {
        String jreDir = getJreDir();
        // jre/lib/ext/*
        String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
        return new WildcardEntry(jreExtPath);
    }

同上。

用户类路径

ini 复制代码
  private Entry parseAppClasspath(String cpOption) {
        if (cpOption == null) {
            cpOption = ".";
        }
        return EntryFactory.create(cpOption);
    }

该路径默认为当前目录,如果命令行里面有设置,那就读取命令行里面的参数。命令行的实现这里不展开,有很多库可以实现命令行参数的解析。

读取类

有了上面的框架,我们就可以开始读取一个类了:

kotlin 复制代码
  public byte[] readClass(String className) throws Exception {
        className = className + ".class";

        try {
            return bootStrapClasspath.readClass(className);
        } catch (Exception ignored) {

        }

        try {
            return extensionClasspath.readClass(className);
        } catch (Exception ignored) {

        }

        return appClasspath.readClass(className);
    }

由于我们加载类的时候,传递的类名是不带后缀的,所以需要先加上,然后再搜索。

结语

本章讨论了Java虚拟机从哪里寻找class文件,对类路径有了较为深入的了解,并且把抽象的类路径概念转变成了具体的代码。下一章将研究class文件格式,实现class文件解析。

相关推荐
hackeroink12 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css