龙空技术网

写自己的JVM(0x1)

二手的程序员 111

前言:

现在你们对“0x1c语言”大致比较注意,大家都想要剖析一些“0x1c语言”的相关文章。那么小编在网络上收集了一些对于“0x1c语言””的相关文章,希望我们能喜欢,同学们快快来了解一下吧!

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

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

下面是原始博客的地址:

该项目的地址:

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

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

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

该项目并不会去实现对象分配与内存管理,有兴趣的可以看看 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 文件在压缩文件里面

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

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

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

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

DirEntry

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

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

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 参数,指定用户类路径,它可以利用; 来配置多个路径。

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:

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

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路径

  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!");    }

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

启动类路径

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

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

扩展类路径

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

同上。

用户类路径

  private Entry parseAppClasspath(String cpOption) {        if (cpOption == null) {            cpOption = ".";        }        return EntryFactory.create(cpOption);    }

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

读取类

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

  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文件解析。

标签: #0x1c语言