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 文件在压缩文件里面
根据这些情况,我们使用组合模式来写代码,看以下组合模式的类图:
我们仿照这个类图,首先定义一个接口:
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文件解析。