文章目录
- 代码目录
- 一、类路径
- 二、实现类路径
- 1、Entry接口及其实现类
- ①DirEntry:
- ②ZipEntry:
- ③CompositeEntry:
- ④WildcardEntry:
- 2、ClassPath
- 三、测试
第1章介绍了java命令如何启动Java应用程序:首先解析命令行参数,启动JVM,将主类加载进JVM,最后调用主类的
main()方法。例如一个HelloWorld程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
加载HelloWorld类之前,首先要加载它的父类,也就是java.lang.object。在调用main()方法之前,因为虚拟机需要准备好参数数组,所以需要加载java.lang.String和java.lang.String[]类。把字符串打印到控制台还需要加载java.lang.System类,等等。若要加载这些类,就必须提到双亲委派机制。
代码目录
ZYX-demo-jvm-02
├── pom.xml
└── src
└── main
│ └── java
│ └── org.ZYX.demo.jvm
│ ├── classpath
│ │ ├── impl
│ │ │ ├── CompositeEntry.java
│ │ │ ├── DirEntry.java
│ │ │ ├── WildcardEntry.java
│ │ │ └── ZipEntry.java
│ │ ├── Classpath.java
│ │ └── Entry.java
│ ├── Cmd.java
│ └── Main.java
└── test
└── java
└── org.ZYX.demo.test
└── HelloWorld.java
一、类路径
Oracle的Java虚拟机实现根据类路径(classpath)来搜索类。按照搜索的先后顺序,类路径可以分为以下3个部分。当 JVM 需要一个类时,就会按照上述顺序在类路径中查找,如果在启动类路径中找不到,就会去扩展类路径寻找,最后才去用户类路径寻找:
·启动类路径(bootstrap classpath):默认对应·jre\lib目录,Java标准库(大部分在rt.jar里)位于该路径。
·扩展类路径(extension classpath):默认对应·jre\lib\ext目录,使用Java扩展机制的类位于这个路径。
·用户类路径(user classpath):我们自己实现的类,以及第三方类库则位于用户类路径,位于当前目录,也可以通过-cp和-classpath指定。
首先我们创建一个classPath子目录。
我们的Java虚拟机将使用JDK的启动类路径来寻找和加载Java标准库中的类,因此需要某种方式指定jre目录的位置。命令行选项是个不错的选择,所以增加一个非标准选项-Xjre。修改Cmd类,增加一个属性:
@Parameter(names = "-Xjre", description = "path to jre", order = 4)
String jre;
JVM 可读取的类路径项,分四种类型:目录形式、压缩包形式(jar 和 zip)、通配符形式和混合形式(就是以上三种的混合体,以路径分隔符分割)。
二、实现类路径
下面我们来实现类路径。
1、Entry接口及其实现类
先定义一个接口Entry来表示类路径项。代码如下:
public interface Entry {
/*此方法留给具体类来实现,负责寻找和加载class文件;
注意这个方法的参数是 class 的相对路径,例如读取 java.lang.Object 类,应传入 java/lang/Object;
*/
byte[] readClass(String className) throws IOException;
//静态方法 create() 根据传入的路径字符串,来判断具体创建哪种实现类;
static Entry create(String path) {
//File.pathSeparator;路径分隔符(win\linux)
if (path.contains(File.pathSeparator)) {
return new CompositeEntry(path);
}
if (path.endsWith("*")) {
return new WildcardEntry(path);
}
if (path.endsWith(".jar") || path.endsWith(".JAR") ||
path.endsWith(".zip") || path.endsWith(".ZIP")) {
return new ZipEntry(path);
}
return new DirEntry(path);
}
}
①DirEntry:
DirEntry相对简单一些,表示目录形式的类路径。DirEntry只有一个字段,用于存放目录的绝对路径。代码如下:
public class DirEntry implements Entry {
private Path absolutePath;
public DirEntry(String path){
//获取绝对路径
this.absolutePath = Paths.get(path).toAbsolutePath();
}
/*利用 java.nio.file.Path 类,可以直接在路径下找到对应文件,
再使用 Files.readAllBytes() 直接读取到字节数组;
*/
@Override
public byte[] readClass(String className) throws IOException {
return Files.readAllBytes(absolutePath.resolve(className));
}
@Override
public String toString() {
return this.absolutePath.toString();
}
}
②ZipEntry:
ZipEntry表示ZIP或JAR文件形式的类路径。这种情况下,class 的相对路径就是压缩内部的目录的路径。代码如下:
public class ZipEntry implements Entry {
private Path absolutePath;
public ZipEntry(String path) {
//获取绝对路径
this.absolutePath = Paths.get(path).toAbsolutePath();
}
//java.nio 包提供了一个 FileSystem 类,可以快速读取压缩包。这个实现就变得非常简单了;
@Override
public byte[] readClass(String className) throws IOException {
try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
return Files.readAllBytes(zipFs.getPath(className));
}
}
@Override
public String toString() {
return this.absolutePath.toString();
}
}
③CompositeEntry:
CompositeEntry
public class CompositeEntry implements Entry {
private final List<Entry> entryList = new ArrayList<>();
public CompositeEntry(String pathList) {
String[] paths = pathList.split(File.pathSeparator);
for (String path : paths) {
entryList.add(Entry.create(path));
}
}
//遍历 entryList 逐个读取;
@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);
}
}
④WildcardEntry:
WildcardEntry 是结尾通配符的类路径,它实际上也就是 CompositeEntry,所以可以直接继承CompositeEntry。
public class WildcardEntry extends CompositeEntry {
public WildcardEntry(String path) {
super(toPathList(path));
}
private static String toPathList(String wildcardPath) {
String baseDir = wildcardPath.replace("*", ""); // remove *
try {
/*将遍历 baseDir 下所有文件,挑选出 jar 包,拼接成字符串之后返回;
在构造中这个字符串传递给父类构造,实际上这个过程就是将通配符转换为多个有效路径。
*/
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 "";
}
}
}
2、ClassPath
Entry接口和4个实现介绍完了,接下来实现Classpath类。Classpath 就用来读取上面提到的三种类路径了,在构造方法中就解析路径并构造 Entry。代买如下:
public class Classpath {
// JVM 启动时必须要加载的三类类路径
private Entry bootstrapClasspath; // 启动类路径
private Entry extensionClasspath; // 扩展类路径
private Entry userClasspath; // 用户类路径
public Classpath(String jreOption, String cpOption) {
// 启动类和扩展类由 jre 提供
parseBootAndExtensionClasspath(jreOption);
// 解析用户自定义的类的路径
parseUserClasspath(cpOption);
}
}
两个 parse
private void parseBootAndExtensionClasspath(String jreOption) {
String jreDir = getJreDir(jreOption);
// 启动类在 jre/lib/*
String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
bootstrapClasspath = new WildcardEntry(jreLibPath);
// 扩展类在 jre/lib/ext/*
String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
extensionClasspath = new WildcardEntry(jreExtPath);
}
private void parseUserClasspath(String cpOption) {
// 如果用户没有提供-classpath/-cp选项,则使用当前目录作为用户类路径
if(cpOption == null) {
cpOption = ".";
}
userClasspath = Entry.create(cpOption);
}
parseBootAndExtensionClasspath中的 getJreDir
/*
* 寻找 jre 路径顺序
* 1. 用户指定路径
* 2. 当前文件夹下的 jre 文件夹
* 3. 系统环境变量 JAVA_HOME 指定的文件夹
*/
private static String getJreDir(String jreOption) {
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("找不到 jre 路径!");
}
最后是ReadClass()方法,它依次从启动类路径、扩展类路径和用户类路径中搜索class文件,代码如下:
public byte[] readClass(String className) throws Exception {
// 根据类名获取一个类的字节码
// 根据双亲委派机制,按顺序读取,并且在前两个读取不到时不会报错
className = className + ".class";
try {
return bootstrapClasspath.readClass(className);
} catch (Exception e) {
// ignored
}
try {
return extensionClasspath.readClass(className);
} catch (Exception e) {
// ignored
}
return userClasspath.readClass(className);
}
三、测试
main() 函数可以不用改动,我们只需要重写 startJVM()
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());
//获取主类目录名
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();
}
}
startJVM()
我们还需要一个主类,本节最开头提到的 HelloWorld
启动参数也需要对应修改为,-Xjre “本机jre路径” “HelloWorld.class的文件路径”。
启动之后输出如下:
classpath:org.ZYX.demo.jvm.classpath.Classpath@7591083d class:D:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-02\src\main\resources\HelloWorld args:null
[-54, -2, -70, -66, 0, 0, 0, 52, 0, 29, 10, 0, 6, 0, 15, 9, 0, 16, 0, 17, 8, 0, 18, 10, 0, 19, 0, 20, 7, 0, 21, 7, 0, 22, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 4, 109, 97, 105, 110, 1, 0, 22, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 7, 0, 8, 7, 0, 23, 12, 0, 24, 0, 25, 1, 0, 13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, 7, 0, 26, 12, 0, 27, 0, 28, 1, 0, 28, 111, 114, 103, 47, 90, 89, 88, 47, 100, 101, 109, 111, 47, 116, 101, 115, 116, 47, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 9, 0, 11, 0, 12, 0, 1, 0, 9, 0, 0, 0, 37, 0, 2, 0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 6, 0, 8, 0, 7, 0, 1, 0, 13, 0, 0, 0, 2, 0, 14]
classData:
以十六进制的形式输出了 HelloWorld 类的内容,虽然这些数据我们还看不懂,但是数据最前面的 8 个十六进制数,是 CAFEBABE,“咖啡宝贝”。了解过字节码的同学应该知道,这正是字节码最开头的 magic number,读取成功。