jvm启动的时候, 并不会一次性加载所有的class文件, 而是根据需要去动态加载. 否则一次性加载那么多jar包那么多class, 那内存将崩溃.
Java 类加载器
Java 类加载流程
Java语言系统自带有三个类加载器, 分别为如下:
BootStrap ClassLoader
Bootstrap ClassLoader: 最顶层的加载类, 主要加载核心类库, %JRE_HOME%\lib
下的rt.jar、resources.jar、charsets.jar和class等,其中的rt.jar包中包含了java.lang, java.io等包, 所以我们可以直接在一个干净的java环境中进行引入FileInputStream, Integer, String类等, 如图:
当然, 我们也可以通过如下代码进行查看Bootstrap ClassLoader
具体扫描了哪些包:
package com.heihu577;
public class HeihuHello {
public static void main(String[] args) {
ClassLoader classLoader = String.class.getClassLoader(); // 得到 String 这个类是由哪个 ClassLoader 加载的. 这里返回 null, 说明是被 BootStrap ClassLoader 所加载了, 我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null。
String searchPath = System.getProperty("sun.boot.class.path"); // sun.boot.class.path 是 bootstrap 所扫描包的路径
System.out.println("当前扫描路径为: " + searchPath);
/*
当前扫描路径为: D:\SoftWare\Java8\jre\lib\resources.jar;D:\SoftWare\Java8\jre\lib\rt.jar;D:\SoftWare\Java8\jre\lib\sunrsasign.jar;D:\SoftWare\Java8\jre\lib\jsse.jar;D:\SoftWare\Java8\jre\lib\jce.jar;D:\SoftWare\Java8\jre\lib\charsets.jar;D:\SoftWare\Java8\jre\lib\jfr.jar;D:\SoftWare\Java8\jre\classes
*/
}
}
当然了, 除了这个固定的扫描包的规则之外, 我们还可以通过-Xbootclasspath参数增加要扫描的包, -Xbootclasspath解释如下:
-Xbootclasspath:路径 指定的路径会完全取代jdk核心的搜索路径
-Xbootclasspath/a:路径 指定的路径会append(追加)在核心搜索路径之后
-Xbootclasspath/p:路径 指定的路径会prefix(之前)在核心搜索路径之后
测试这三种结果, 再测试一个没有增加该参数的情况:
通常都使用 /a 参数, 该扫描路径有一个先后顺序问题, 当前后两个jar包中, 存在两个相同包相同名称的类时, 先被扫描到的包下的类, 将被 JVM 解析.
Ext ClassLoader
扩展的类加载器,加载目录%JRE_HOME%\lib\ext
目录下的jar包和class文件。还可以加载-D java.ext.dirs
选项指定的目录。
根据上面的案例, 我们进行如下操作:
随后准备如下代码:
package com.heihu577;
import org.apache.commons.dbutils.AbstractQueryRunner;
public class HeihuHello {
public static void main(String[] args) {
ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader(); // 得到加载 AbstractQueryRunner 类的 ClassLoader
System.out.println(classLoader); // sun.misc.Launcher$ExtClassLoader@29453f44
String extDirs = System.getProperty("java.ext.dirs"); // ExtClassLoader 通过 java.ext.dirs 查看扫描路径
System.out.println(extDirs); // D:\SoftWare\Java8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
}
}
可以看到, 此时classLoader则变为了ExtClassLoader所加载进来的, 当然, 我们也可以指明-D
参数来修改ExtClassLoader扫描的路径, 如图:
注意: 这里图中应该改为配置dirs D:\BaiduNetdiskDownload\
最后运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
D:\BaiduNetdiskDownload\
这种方式当然也不要随意用, 只是做环境测试.
App ClassLoader
这里我们App ClassLoader其实读取的就是我们的CLASSPATH, 当然也是我们IDEA中运行代码时所指明的参数, 准备如下代码:
public class HeihuHello {
public static void main(String[] args) {
ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader();
System.out.println(classLoader);
String myClassPath = System.getProperty("java.class.path");
System.out.println(myClassPath);
}
}
运行结果 (包含命令行):
# 注意运行命令中的 -classpath 参数
D:\SoftWare\Java8\bin\java.exe "-javaagent:D:\SoftWare\IntelliJ IDEA 2023.2.1\lib\idea_rt.jar=8196:D:\SoftWare\IntelliJ IDEA 2023.2.1\bin" -Dfile.encoding=UTF-8 -classpath CLASSPATH值 com.heihu577.HeihuHello
# 运行结果
sun.misc.Launcher$AppClassLoader@18b4aac2
CLASSPATH值
进程已结束,退出代码为 0
当然了, AppClassLoader的父亲(不是父类)是ExtClassLoader:
public class HeihuHello {
public static void main(String[] args) {
ClassLoader classLoader = AbstractQueryRunner.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(classLoader.getParent()); // sun.misc.Launcher$ExtClassLoader@12a3a380
}
}
理解完这三种 ClassLoader 后, 笔者在这里准备了一个本地脚本, 用来运行并理解每次clazz.getClassLoader的返回值:
package com.heihu577;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("================================BootStrapClassLoader================================");
String envKey = "sun.boot.class.path";
String envValue = System.getProperty(envKey);
String bootStrapClassName = "java.lang.Integer"; // 默认在 lib\rt.jar 包中
System.out.println(envKey + " => " + envValue); // 得到 BootStrap 加载的所有 jar 包
System.out.println(bootStrapClassName + " => " + Class.forName(bootStrapClassName).getClassLoader()); // 因为是BootStrap加载, 所以这里应该返回 NULL
System.out.println("--------------------------------运行时指明参数----------------------------------");
System.out.println("-Xbootclasspath:路径 指定的路径会完全取代jdk核心的搜索路径\n" +
"-Xbootclasspath/a:路径 指定的路径会append(追加)在核心搜索路径之后\n" +
"-Xbootclasspath/p:路径 指定的路径会prefix(之前)在核心搜索路径之后");
System.out.println("================================ExtClassLoader================================");
envKey = "java.ext.dirs";
envValue = System.getProperty(envKey);
String extClassName = "com.sun.nio.zipfs.JarFileSystemProvider"; // 默认在 /lib/ext/zipfs 包中
System.out.println(envKey + " => " + envValue);
System.out.println(extClassName + " => " + Class.forName(extClassName).getClassLoader());
// 因为是 ExtClassLoader 加载, 所以是 sun.misc.Launcher$ExtClassLoader
System.out.println("--------------------------------运行时指明参数----------------------------------");
System.out.println("-Djava.ext.dirs=目录");
System.out.println("================================AppClassLoader================================");
envKey = "java.class.path";
envValue = System.getProperty(envKey);
System.out.println(envKey + " => " + envValue);
String appClassName = "lombok.Data";
System.out.println(appClassName + " => " + Class.forName(appClassName).getClassLoader());
/*
* <dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
* */
}
}
接下来抛出一个问题, AppClassLoader, ExtClassLoader是如何被创建的?
Launcher
上面的代码可以看到, AppClassLoader, ExtClassLoader都是属于sun.misc.Launcher类中的一个成员类, 我们看一下Launcher类的具体操作如下: