环境
操作系统:Windows 10
IDE:IntelliJ IDEA 2019.1 x64
JDK:Java 11.0.8
项目管理工具:apache-maven-3.5.4 [下载]
可能遇到的问题
可能很多工具函数看不懂,没事,都注释在代码中了。
开始
simpleJVM-2
├── pom.xml
└── src
└── main
│ └── java
│ └── cn.zwy.simplejvm
│ ├── classpath
│ │ ├── impl
│ │ │ ├── CompositeEntry.java
│ │ │ ├── DirEntry.java
│ │ │ ├── WildcardEntry.java
│ │ │ └── ZipEntry.java
│ │ ├── Classpath.java
│ │ └── Entry.java
│ ├── Cmd.java
│ └── Main.java
└── test
└── java
└── cn.zwy.test
└── HelloWorld.java
在Java中,Object类是所有类的父类。
我们要运行一个class文件,要先把class文件加载到JVM中,并且还要加载其中用到的像Object类这样的父类。执行的流程大概可以概括为:
解析命令行参数 -> 启动JVM -> 把类加载进JVM -> 调用main方法
由于双亲委派模型的存在,我们需要保证启动类加载器,在启动类路径加载java库,因此有参数-Xjre
来指定jre目录的位置:
Cmd.java
package cn.zwy.simplejvm;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import java.util.List;
/**
* @author weiyu_zeng
*
* Parameter函数参数:
* names 设置命令行参数,如-old
* required 设置此参数是否必须
* description 设置参数的描述
* order 设置帮助文档的顺序
* help 设置此参数是否为展示帮助文档或者辅助功能
*
* -Xjre: jre目录位置指定
*/
public class Cmd {
@Parameter(names = {"-?", "-help"}, description = "print help message", order = 3, help = true)
boolean helpFlag = false;
@Parameter(names = "-version", description = "print version and exit", order = 2)
boolean versionFlag = false;
@Parameter(names = {"-cp", "-classpath"}, description = "classpath", order = 1)
String classpath;
@Parameter(names = {"-Xjre"}, description = "path to jre", order = 4)
String jre;
@Parameter(description = "main class and args")
List<String> mainClassAndArgs;
boolean ok;
// 返回mainClassAndArgs的Class(List<String>的第一个参数)
String getMainClass() {
return (mainClassAndArgs != null && !mainClassAndArgs.isEmpty())
? mainClassAndArgs.get(0) : null;
}
List<String> getAppArgs() {
return (mainClassAndArgs != null && mainClassAndArgs.size() > 1)
? mainClassAndArgs.subList(1, mainClassAndArgs.size()) : null;
}
// 解析函数:传入String[] argv,返回Cmd类
static Cmd parse(String[] argv) {
Cmd args = new Cmd();
JCommander cmd = JCommander.newBuilder().addObject(args).build();
cmd.parse(argv);
args.ok = true;
return args;
}
}
JVM 可读取的类路径项,有四种类型:
a.目录形式(DirEntry.java)
b.压缩包形式(jar, zip)(ZipEntry.java)
c.通配符形式 (WildcardEntry.java)
d.混合形式(就是以上三种的混合,以路径分隔符分割)(CompositeEntry.java)
我们创建四个类路径文件,先放着:
然后我们来定义这四个类路径的接口,Entry.java
package cn.zwy.simplejvm.classpath;
import cn.zwy.simplejvm.classpath.impl.CompositeEntry;
import cn.zwy.simplejvm.classpath.impl.DirEntry;
import cn.zwy.simplejvm.classpath.impl.ZipEntry;
import cn.zwy.simplejvm.classpath.impl.WildcardEntry;
import java.io.IOException;
import java.io.File;
/**
* @author weiyu_zeng
* 类路径接口
*
* java.io.File: File.pathSeparator字段,代表系统目录中的间隔符/分隔符 "/" "\",能够兼容不同系统(window,Unix等)
*/
public interface Entry {
byte[] readClass(String className) throws IOException;
static Entry create(String path) {
// 包含以分隔符分割的多种组合路径
if (path.contains(File.pathSeparator)) {
return new CompositeEntry(path);
}
// 通配符
if (path.endsWith("*")) {
return new WildcardEntry(path);
}
// jar包或zip包
if (path.endsWith(".jar") || path.endsWith("JAR") || path.endsWith(".zip") || path.endsWith(".ZIP")) {
return new ZipEntry(path);
}
// 目录
return new DirEntry(path);
}
}
然后定义四种类路径class:
首先是最简单的目录形式的类路径,DirEntry.java
package cn.zwy.simplejvm.classpath.impl;
import cn.zwy.simplejvm.classpath.Entry;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author weiyu_zeng
* 目录形式的类路径
*
* java.nio.file.Paths: get()方法,用来获取 Path 对象,可以将多个字符串串连成路径
* java.nio.file.Path: toAbsolutePath()方法,作为绝对路径返回调用 Path 对象
* resolve(Path p)方法,将相对路径解析为绝对路径
*
* java.nio.file.Files: readAllBytes(Path path)方法,读取文件中的所有字节。
*/
public class DirEntry implements Entry {
private final Path absolutePath;
public DirEntry(String path) {
// 获取绝对路径
this.absolutePath = Paths.get(path).toAbsolutePath();
}
@Override
public byte[] readClass(String className) throws IOException {
return Files.readAllBytes(absolutePath.resolve(className));
}
@Override
public String toString() {
return this.absolutePath.toString();
}
}
压缩包形式类路径,ZipEntry.java
package cn.zwy.simplejvm.classpath.impl;
import cn.zwy.simplejvm.classpath.Entry;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.Files;
/**
* @author weiyu_zeng
* zip/jar包形式类路径
*
* java.nio.file.FileSystems: newFileSystem(Path path, ClassLoader loader)方法,构建一个文件系统
* 去访问path指定的文件的内容,该专门用来创建虚拟文件系统的providers。
* 这里获取FileSystem实例。
* java.nio.file.FileSystem: getPath(String first,String... more)方法, 将字符串拼接成一个路径,
* 并根据路径生成Path的实例。
*/
public class ZipEntry implements Entry {
private Path absolutePath;
public ZipEntry(String path) {
// 得到绝对路径
this.absolutePath = Paths.get(path).toAbsolutePath();
}
@Override
public byte[] readClass(String className) throws IOException {
try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
return Files.readAllBytes(zipFs.getPath(className));
}
}
}
包含以分隔符分割的多种组合路径,CompositeEntry:
package cn.zwy.simplejvm.classpath.impl;
import cn.zwy.simplejvm.classpath.Entry;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author weiyu_zeng
* 包含以分隔符分割的多种组合路径
*
* 构造函数CompositeEntry:获取包含以分隔符分割的多种组合路径的绝对路径
* readClass方法:赋予Entry中readClass方法的实现细节,读取CompositeEntry的class文件
*/
public class CompositeEntry implements Entry {
private final List<Entry> entryList = new ArrayList<>();
public CompositeEntry(String pathList) {
String[] paths = pathList.split(File.pathSeparator); // 以分隔符分隔开path,一个元素是单个的path
for (String path: paths) {
entryList.add(Entry.create(path)); // 套娃:再把单个的path传入Entry.create,最后就会当做目录形式的类路径
// DirEntry来调用。所以其实CompositeEntry底层也还是DirEntry。
}
}
@Override
public byte[] readClass(String className) throws IOException {
for (Entry entry: entryList) {
try {
return entry.readClass(className);
} catch (Exception ignored) {
// ignored
}
}
throw new IOException("class not found" + className);
}
@Override
public String toString() {
String[] strs = new String[entryList.size()];
for (int i = 0; i < entryList.size(); i++) {
strs[i] = entryList.get(i).toString();
}
return String.join(File.pathSeparator, strs); // 把strs中的元素用File.pathSeparator连接起来
}
}
包含通配符的路径,WildcardEntry.java:
package cn.zwy.simplejvm.classpath.impl;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
/**
* @author weiyu_zeng
* 包含通配符的路径
*
* Files.walk:传入Path类型,返回Stream<Path>类型。Files.walk(Paths.get(...))可以看做固定搭配。
* 列出Path中所有的文件。加filter之后可以列出特定格式的文件。
* "::" :A :: B,等于调用A的B方法。
* Stream流中的常用方法filter:用于对Stream流中的数据进行过滤,Stream<T> filter(Predicate<? super T> predicate);
* filter方法参数Predicate是一个函数式接口,可传递Lambda表达式,对数据进行过滤。
* Stream流中的常用方法map:将对象转换为其他对象,常和正则式一起用。
* Stream流中的常用方法collect:在这里将Stream对象转为String对象。Stream对象.collect(Collectors.joining("某字符"))可以看做
* 固定搭配。实际就是把返回的String对象用"某字符"连起来。
*
* 构造函数WildcardEntry:获取包含以分隔符分割的多种组合路径的绝对路径
* readClass方法:赋予Entry中readClass方法的实现细节,读取CompositeEntry的class文件
*/
public class WildcardEntry extends CompositeEntry {
public WildcardEntry(String path) {
super(toPathList(path));
}
private static String toPathList(String wildcardPath) {
String baseDir = wildcardPath.replace("*", ""); // 去掉"*"符号
try {
return Files.walk(Paths.get(baseDir))
.filter(Files::isRegularFile)
.map(Path::toString)
.filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
.collect(Collectors.joining(File.pathSeparator));
} catch (IOException e) {
return "";
}
}
}
因为有"*"的存在,所以这里要Files.walk来搜索路径下所有的文件,加载到jvm中。
ClassPath.java
package cn.zwy.simplejvm.classpath;
import cn.zwy.simplejvm.classpath.impl.WildcardEntry;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* @author weiyu_zeng
* 类路径
*
* System.getenv():方法是获取指定的环境变量的值。接收参数为任意字符串,当存在指定环境变量时即返回环境变量的值,否则返回null。
* Files.exits():方法用来检查给定的Path在文件系统中是否存在
*/
public class Classpath {
private Entry bootstrapClasspath; // 启动类路径
private Entry extensionClasspath; // 拓展类路径
private Entry userClasspath; // 用户类路径
public Classpath(String jreOption, String cpOption) {
//启动类&扩展类 "C:\Program Files\Java\...\jre"
parseBootAndExtensionClasspath(jreOption);
//用户类 E:\..\test\java\cn\zwy\test\HelloWorld
parseUserClasspath(cpOption);
}
// 解析启动类路径和拓展类路径
private void parseBootAndExtensionClasspath(String jreOption) {
String jreDir = getJreDir(jreOption);
// 启动类加载器负责 jre/lib 目录下类库。有通配符用WildcardEntry
String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
bootstrapClasspath = new WildcardEntry(jreLibPath);
// 拓展类加载器负责 jre/lib/ext 目录下的类库。有通配符用WildcardEntry
String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
extensionClasspath = new WildcardEntry(jreExtPath);
}
// 获取jre路径
private static String getJreDir(String jreOption) {
/*
* 寻找 jre 路径顺序
* 1. 用户指定路径
* 2. 当前文件夹下的 jre 文件夹
* 3. 系统环境变量 JAVA_HOME 指定的文件夹
* 作者:何人听我楚狂声
*/
if (jreOption != null && Files.exists(Paths.get(jreOption))) {
return jreOption;
}
if (Files.exists(Paths.get("./jre"))) {
return "./jre";
}
String jh = System.getenv("JAVA_HOME");
if (jh != null) {
return Paths.get(jh, "jre").toString();
}
throw new RuntimeException("Can not find JRE folder!!");
}
// 解析用户类路径
private void parseUserClasspath(String cpOption) {
if (cpOption == null) {
cpOption = ".";
}
userClasspath = Entry.create(cpOption);
}
// 双亲委派机制,先从启动类加载器加载,加载不出不报错。
public byte[] readClass(String className) throws Exception {
className = className + ".class";
// 启动类路径
try {
return bootstrapClasspath.readClass(className);
} catch (Exception ignored) {
// ignored
}
// 拓展类路径
try {
return extensionClasspath.readClass(className);
} catch (Exception ignored) {
// ignored
}
// 用户类路径
return userClasspath.readClass(className);
}
}
我们把之前的HelloWorld,用javac解析成class文件
找到文件位置,右键,open in terminal
然后命令行里输入javac HelloWorld.java
得到的HelloWorld.class文件复制到src\main\resources下
重写一下Main.java
package cn.zwy.simplejvm;
import cn.zwy.simplejvm.classpath.Classpath;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Cmd cmd = Cmd.parse(args);
if (!cmd.ok || cmd.helpFlag) {
System.out.println("Usage: <main class> [-options] class [args...]");
return;
}
if (cmd.versionFlag) {
System.out.println("java version \"1.8.0\"");
return;
}
startJVM(cmd);
}
// %s链接后面的变量
// 分别打印classpath, getMainClass()返回值,和getAppArgs()返回值
private static void startJVM(Cmd cmd) {
Classpath cp = new Classpath(cmd.jre, cmd.classpath);
System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
//获取className
String className = cmd.getMainClass().replace(".", "/");
try {
byte[] classData = cp.readClass(className);
// System.out.println(Arrays.toString(classData));
System.out.println("classData:");
for (byte b : classData) {
//16进制输出
System.out.print(String.format("%02x", b & 0xff) + " ");
}
} catch (Exception e) {
System.out.println("Could not find or load main class " + cmd.getMainClass());
e.printStackTrace();
}
}
}
执行
打开
在这里我们填入,-Xjre "你的jre的路径" 你的HelloWorld.class的路径
:
-Xjre "D:\Other Program\JAVA\jre" D:\Project\JAVA_simpleJVM\simpleJVM-2\src\main\resources\HelloWorld
如果你跟我一样是java11,那么java11是不自带jre的,你还需要把jre弄出来,可见这里
有jre之后,运行Main.java:
完成。
参考
https://bugstack.cn/itstack-demo-jvm/itstack-demo-jvm.html
https://www.nowcoder.com/discuss/604388?source_id=profile_create_nctrack&channel=-1
《自己动手写Java虚拟机》
https://github.com/Xtarling/JVM-Demo。
https://www.cnblogs.com/fengli9998/p/9002377.html
https://geek-docs.com/java/java-tutorial/fileswalk.html#Fileswalk
https://blog.csdn.net/zebe1989/article/details/83054037