Java-JVM-类加载器
1.背景
一个.java
文件如何运行起来的。
在此之前,我们先了解下一些基本知识。
1.1 lib和dll
大致就是说,lib是编译时用到的,dll是运行时用到的。
在咱们的jdk文件夹中搜索,是能找到一个jvm.dll
文件的。
1.2 java.exe
jre文件下一般有个java可执行文件。
java.exe
是 Java 开发工具包(JDK)中的一个重要可执行文件,用于启动和运行 Java 应用程序。
它的主要作用是启动 Java 虚拟机(JVM),加载指定的类,并执行该类的 main
方法。
像linux下,就是一个名为java的可执行文件。
2.类加载器
类加载器,无非就是加载我们的class文件到jvm中,方便后续使用,先来记住几个特点。
- 类加载的目的是将类class文件读入内存,并为之创建一个java.lang.Class对象。
- class文件被载入到了内存之后,才能被其它class所引用。
- jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。
- 类的唯一性由类加载器和类共同决定
下面列举了Java中的几个类加载器:
序号 | 类加载器 | 英文名 | 描述 | 典型加载位置 |
---|---|---|---|---|
1 | 引导类加载器 | Bootstrap ClassLoader | JVM 内置的最顶层类加载器,由本地代码实现,不是 java.lang.ClassLoader 的子类。负责加载核心类库。 |
通常是 <JAVA_HOME>/jre/lib 目录中的 rt.jar 、resources.jar 等核心类库 |
2 | 扩展类加载器 | Extension ClassLoader | 引导类加载器的子类加载器,负责加载 JRE 扩展目录中的类库。实现为 sun.misc.Launcher$ExtClassLoader 。 |
通常是 <JAVA_HOME>/jre/lib/ext 目录中的 JAR 文件 |
3 | 应用程序类加载器 | Application ClassLoader 或 System ClassLoader | 扩展类加载器的子类加载器,负责加载应用程序类路径中的类。实现为 sun.misc.Launcher$AppClassLoader 。 |
由 -cp 或 -classpath 参数指定的类路径中的类和 JAR 文件 |
4 | 自定义类加载器 | Custom ClassLoaders | 用户根据需要创建的类加载器,继承自 java.lang.ClassLoader ,可以定义自己的类加载逻辑。 |
取决于用户定义,通常用于加载特殊格式或特殊位置的类 |
好,什么类加载器这种鸟语啊,抽象的很,理解成读取class文件的工具类就行了。
然后我们要注意一下,毕竟Java启动了肯定要有个玩意初始化最开始的class啊,先得把鸡抓过来才能生蛋。
-
最上面的引导类加载器(根加载器)是由咱们的虚拟机启动的,它先把重要的类都加载好,比如咱们的
Object
类(位于rt.jar
)。 -
根加载器同时加载了一个很重要的类
sun.misc.Launcher
(也位于rt.jar
),这个玩意的用处之一就是启动咱们的扩展类加载器和应用程序类加载器。
2.1 引导类加载器
- JVM创建引导类加载器(Bootstrap ClassLoader),用于加载核心 Java 类库。
- 引导类加载器加载并初始化 JVM 需要的基础类(如
java.lang
、java.util
、java.io
、java.net
等)。
额,这个引导类加载器要加载啥呢,大概就是jre\lib
下面的那堆。
public class HelloWorld {
public static void main(String[] args) {
// 获取并打印引导类加载器的加载路径
String bootstrapClassPath = System.getProperty("sun.boot.class.path");
System.out.println("Bootstrap Class Loader Path:");
for (String path : bootstrapClassPath.split(";")) {
System.out.println(path);
}
}
}
输出结果:
Bootstrap Class Loader Path:
D:\Toolkit\jdk\jdk-1.8\jre\lib\resources.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\rt.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\sunrsasign.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jsse.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jce.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\charsets.jar
D:\Toolkit\jdk\jdk-1.8\jre\lib\jfr.jar
D:\Toolkit\jdk\jdk-1.8\jre\classes
C:\Users\Administrator\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar
像我们的Object
类,就在这个rt.jar
中。
2.2 扩展类加载器
这个玩意呢,就是咱们jre\lib\ext
文件夹下的一些东西。
public static void main(String[] args) {
String extClassPath = System.getProperty("java.ext.dirs");
System.out.println("ExtClassPath Class Loader Path:");
for (String path : extClassPath.split(";")) {
System.out.println(path);
}
}
输出结果:
ExtClassPath Class Loader Path:
D:\Toolkit\jdk\jdk-1.8\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
2.3 应用程序类加载器
应用程序类加载器,就是要加载咱们自己写的代码。平时咱们习惯在idea中编码,实际上是idea帮我们传递进去的。
public static void main(String[] args) {
String appClassPath = System.getProperty("java.class.path");
System.out.println("AppClassPath Class Loader Path:");
for (String path : appClassPath.split(";")) {
System.out.println(path);
}
}
3.扩展
3.1 Launcher类
上文提到了,Launcher
类中初始化了扩展类加载器和应用程序类加载器。
可以在Launcher类中看到这两个内部类。
这个try语句真的很影响阅读啊,哎,我给你简化下。
public Launcher() {
ExtClassLoader var1;
// 获取扩展类加载器
var1= Launcher.ExtClassLoader.getExtClassLoader();
// 设置AppClassLoader的父加载器为ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
// ...
哈哈,这几行代码看懵了吧?尤其是!
// 设置AppClassLoader的父加载器为ExtClassLoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
你这个注释骗人的吧,getAppClassLoader()
这方法咋还把extClassLoader传进去了?我们查看下这个方法。
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
// 传入一个类加载器,返回一个新的类加载器
// 传入的这个类加载器会被设置为返回值的这个加载器的父类加载器
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new AppClassLoader(var1x, var0);
}
});
}
// ...
其他的你都不用关心,你就看这行返回的。
return new AppClassLoader(var1x, var0);
然后就是一顿点点点,我帮你点完了。
AppClassLoader
URLClassLoader
SecureClassLoader
ClassLoader
最后就到了这个顶层的ClassLoader类,然后赋值成员变量parent为这个传入的ClassLoader。
所以,我们现在知道。
public static ClassLoader getAppClassLoader(final ClassLoader var0)
返回的这个新的ClassLoader中,其中有个parent字段,值就是我们传入的这个形参ClassLoader var0。
哎,怎么验证,不妨试试。
public static void main(String[] args) {
ClassLoader classLoader = Launcher.getLauncher().getClassLoader();
System.out.println();
}
debug可以看到,Launch类中的这个loader。
3.1 Launch类设置自己的loader为AppClassLoader
前面提到,Launch类是由根加载器加载的,然后,我们根据上面的代码可以看到,它又将自己的loader属性设置为了AppClassLoader
。
注意这里的getAppClassLoader实际上返回的是个AppClassLoader
,只不过它把自己的父类加载器设置成了extClassLoader,前文刚讲了为啥哦。
所以,哇靠,明明是根加载器把你加载出来的,你反手给自己的loader设置成AppClassLoader干嘛?
3.2 扩展类加载器的parent又在哪设置的
翻看Launch类的代码,似乎没有找到设置扩展类父类加载器的这种操作,根据前面的知识我们可以知道,扩展类的父类加载器是根加载器。
额,谁搞的?
JVM设置的,在创建扩展类加载器时,将引导类加载器(null
表示引导类加载器)作为其父类加载器。
3.2 双亲委派
双亲委派机制(Parent-Delegate Model)是Java类加载器中采用的一种类加载策略。
如果一个类加载器收到了类加载请求,默认先将该请求委托给其父类加载器处理。只有当父级加载器无法加载该类时,才会尝试自行加载。
主要目的是以下两点:
- 避免核心类被修改
- 避免类被重复加载
这两个比较好理解,这样我们自己写的java.lang.String
类,别人是不认的,因为根加载器那里就加载出来String类了。
下面从代码层面来看下,加载类的时候,咋就从父类加载器先加载了。
2.2.1 ExtClassLoader
从extClassLoader类声明中,我们可以看到它继承了URLClassLoader。
它的loadClass
方法,是继承的直接父类URLClassLoader的。
最后实际上用的还是ClassLoader
类的loadClass方法。
这里还要注意一个点啊,最开始的那几行代码。
Class<?> c = findLoadedClass(name);
if (c == null) {
// ...
}
这里,也说明了,一个类咱们只会加载一次,如果加载过,直接就返回了。
2.2.2 AppClassLoader
AppClassLoader同样继承自URLClassLoader,不过它重写了loadClass
方法。但是if嘛,就是加了些判断而已,最后还是会回到super.loadClass()
。
4.实例分析
以HelloWorld.java为例。
4.1 编写代码
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
4.2 编译代码
使用 javac
编译 Java 程序,生成字节码文件 HelloWorld.class
:
javac HelloWorld.java
4.3 执行代码
java HelloWorld
4.4 解析命令行参数
java.exe
解析命令行参数,确定要运行的主类 HelloWorld
。
4.5 启动 JVM
-
java.exe
调用底层的 JVM 动态链接库(如jvm.dll
),创建并初始化 JVM 实例。 -
具体步骤包括设置 JVM 启动参数(如类路径)并调用
JNI_CreateJavaVM
函数等。
4.6 引导类加载器
引导类加载器,加载核心类。
4.7 应用程序类加载器
sun.misc.Launcher
中初始化的应用程序类加载器(AppClassLoader
),加载用户编写(我们编写)的HelloWorld
类。
4.8 加载主类 HelloWorld
- 应用程序类加载器根据指定的主类名
HelloWorld
,在类路径中查找并加载HelloWorld.class
文件。 - 类加载器将
HelloWorld
类的字节码加载到内存中,并进行解析和初始化。
4.9 查找并调用 main 方法
- JVM 查找
HelloWorld
类中的public static void main(String[] args)
方法。 - JVM 调用
main
方法,开始执行HelloWorld
程序。
4.10 程序结束
main
方法执行完毕,Java 程序运行结束。- JVM 进行资源清理,释放内存,终止进程。
5.扩展
经典章节,背背面试题。
5.1 为啥Tomcat重新类加载器啊?
首先,咱们的tomcat里面可能部署了好几个服务,那里面可能有相同的类。
- 应用1:
cn.yang37.Hello
- 应用2:
cn.yang37.Hello
这两个Hello类不一样很正常吧?字段、方法等等都可能不一样。
现在,按照默认的加载逻辑,应用1加载好了Hello类后,应用2中的Hello类就不能加载了。这个时候呢很简单,应用2等死就好了吧。
所以,tomcat里重写了类加载器,针对不同的应用使用不同的类加载器。
核心点在于:实现类加载的隔离
还记得之前说过一句话嘛,类的唯一性由类加载器和类共同决定,你看咱们的Class类,里面是有这个属性的。
5.2 怎么找到jar里的main方法的?
当你创建一个可执行的JAR包时,必须在其META-INF/MANIFEST.MF
文件中指定一个入口点。
这个入口点是通过Main-Class
属性来定义的,例如:
Main-Class: com.example.MainClass
这个属性告诉JVM哪个类包含main
方法。
比如,我们打开一个SpringBoot的jar包。
Manifest-Version: 1.0
Implementation-Title: yangbbs
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.yang.demo.YangbbsApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.4.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher
哎,这里可以看到,Main-Class是org.springframework.boot.loader.JarLauncher
。
public static void main(String[] args) throws Exception {
(new JarLauncher()).launch(args);
}
嗯,这里将args参数传入,调用的launch()方法,注意这里的Launch类是同包下的。