- 一:Java 的类加载过程
- 二:Java 类加载器
- 1. 引导类加载器(BootStrap ClassLoader)
- 2. 扩展类加载器(Ext ClassLoader)
- 3. 应用程序类加载器(App ClassLoader)
- 4. 自定义类加载器 (Custom ClassLoader)
- 5.类加载器初始化过程
- 三:类加载器加载机制 – 双亲委派机制
- 四:常见面试题
一:Java 的类加载过程
在说类加载器之前,先说一下 Java
的类加载过程,既然是做 Java
的牛逼程序员,总不能面试的时候就说直接 java -jar
运行 jar 包
或者 war 包
不就完了,面试嘛 。。。咱们肯定要高大上一点,开始下面的装逼模板(别墨迹,直接背就行):
->
执行 java 命令->
调用脚本文件创建 Java 虚拟机(C++实现) -> 创建引导类加载器(C++实现) -> C++ 通过 JNI 的方式调用 Java 代码(sun.misc.Launcher.getLauncher() ),创建 JVM 启动实例(Launcher 的实例) -> 获取自己的类加载器(AppClassLoader 的实例):launcher.getClassLoader() -> 调用 loadClass 加载要运行的类 :classLoader.loadClass("com.havemail.demo.XXX") -> C++ 发起调用,执行 main 方法 -> Java 程序运行结束,JVM 销毁
loadClass 的类加载过程主要分为:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
。其中验证、准备与解析
也可以称为连接
过程。
- 加载:加载字节码文件(class 文件),用到哪些类就加载哪些 class 文件;在加载过程中会在内存中生成每个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
- 验证:验证字节码文件的正确性,安全性等。
- 准备:给类的静态变量分配内存,并赋予默认值,这些变量所使用的内存都将在方法区中进行分配 。
- 解析:将常量池内的符号引用(比如 main 方法)替换为直接引用(内存的指针或句柄等)的过程,该过程为
静态链接
过程。 - 初始化:执行静态代码块,对类的静态变量初始化为指定的值;将默认值换成声明类变量时指定的初始值。
二:Java 类加载器
Java 主要提供了下面几种类加载器(3个默认1个自定义):
1. 引导类加载器(BootStrap ClassLoader)
- 负责加载
%JAVA_HOME%/jre/lib
目录下的核心类库,比如rt.jar
、charsets.jar
等; - 负责加载
%JAVA_HOME%/jre/classes
中的类。 - 由 JVM 底层的 C/C++ 实现,Java 代码访问不到该加载器。
- 加载
-Xbootclasspath
参数指定的路径。
可以通过代码查看该类加载器加载了哪些 jar 包:
public class Main { public static void main(String[] args) { URL[] url = Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < url.length; i++) { System.out.println(url[i].toExternalForm()); } } }
输出结果如下所示:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/classes
2. 扩展类加载器(Ext ClassLoader)
全称:Extensions ClassLoader
- 加载 Java 的扩展列库,默认加载
%JAVA_HOME%/jre/lib/ext
扩展目录中的 jar 文件以及由 java.ext.dirs 系统变量指定的路径下的 jar 文件。 - Ext ClassLoader 是 App ClassLoader 的父加载器(并不是说的父类)。
扩展类加载器加载的路径可以通过代码查看:
public class Main {
public static void main(String[] args) {
System.out.println(System.getProperty("java.ext.dirs"));
}
}
输出如下:
/Users/roc/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
3. 应用程序类加载器(App ClassLoader)
【注】因为通过 ClassLoader.getSystemClassLoader()
获得的是 App ClassLoader
,也可称为 系统类加载器
。
- 负责加载 ClassPath 路径下的类包与文件,主要是加载开发人员编写的类文件。
- Java 程序默认的类加载器。
- 加载
-classpath
指定的类包与文件。 - 如果没有特别指定,则用户自定义的任何类加载器都会将该加载器作为它的父加载器。
通过代码可以获取 ClassPath 的加载路径:
public class Main {
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
}
}
输出的结果有点多,仅放关键的部分:
.... /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/tools.jar:/Users/roc/Downloads/HelloJava/out/production/HelloJava:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar
【注】在 IDEA 中如果是纯 Java 项目则为 out
目录,J2EE 项目则为 target
目录。
4. 自定义类加载器(Custom ClassLoader)
- 加载用户自定义路径下的类包。可以通过继承自 java.lang.ClassLoader 类的方式来实现,以满足特定的场景。
5. 类加载器初始化过程
先看源码再说:
public class Launcher { // ① private static Launcher launcher = new Launcher(); private ClassLoader loader; // ... // ④ public static Launcher getLauncher() { return launcher; } public Launcher() { Launcher.ExtClassLoader var1; try { // ② var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { // ③ this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // ... } public ClassLoader getClassLoader() { return this.loader; } // ... }
具体过程如下:
① 当系统启动的时候会创建一个 Launcher 的静态实例(采用了单例模式,保证只有一个实例);
② Launcher 的构造方法会创建一个 ExtClassLoader 的实例,
③ 然后将 ExtClassLoader 的实例传入到 AppClassLoader 作为其父加载器并返回 AppClassLoader 实例,然后赋值给 loader
④ 然后 C++ 通过 JNI 调用 Launcher 类中的 getLauncher() 方法获取到实例。
【注】JVM 默认使用 getClassLoader() 方法返回的类加载器(AppCLassLoader 的实例)来加载我们的应用,所以 AppClassLoader 是 Java 的默认类加载器。
三:类加载器加载机制 – 双亲委派机制
上面说的仅仅是类加载器的初始化流程,接下来就是类加载器的加载机制(也就是当虚拟机需要加载某个类了,这个类如何加载到 JVM 中的过程),Java 的类加载机制使用的是 – 双亲委派机制
,那么什么是双亲委派机制呢?
【注】
- 类加载器加载类的时候,并不会将所有的类完全加载,在程序启动的时候仅仅加载用到的类。
- 加载器加载返回的内容是:在内存中生成的这个类的
java.lang.Class
对象
- 我们自己写了一个 Demo 类,然后运行的时候用到了这个类。 - JVM 想要加载 Demo 类,先让应用程序类加载器加载,应用程序类加载器从已加载的类中查找,如果有则直接返回;没有的话,然后应用程序类会委托给扩展类加载器来加载(委托); - 扩展类加载器收到后,从其已加载的类中查找,如果有则直接返回;没有的话,然后会委托给启动类加载器来加载(委托); - 启动类加载器收到后,从其已加载的类中查找 Demo 类,找到了直接返回;找不到就从自己的加载路径中加载 Demo 类,找到了直接返回,找不到则将请求派给扩展类加载器(派遣); - 扩展类加载器收到后,就从自己的加载路径中加载 Demo 类,找到了直接返回,找不到则将请求派给应用类加载器(派遣); - 应用类加载器收到后,则从自己的加载路径中加载 Demo 类,找到了直接返回,如果找不到则会抛出异常:ClassNotFound。
这里是拿App ClassLoader
、Ext ClassLoader
与 Bootstrap ClassLoader
举例,如果有自定义加载器的话也是一样的道理,默认 AppClassLoader
是 自定义加载器
的父加载器
,他们四者的层级关系入下图所示:
【注】简单一句话总结:双亲委派机制就是先找父亲加载,不行再由儿子自己加载;引导类加载器没有上一级,只能是加载。
四:常见面试题
1. 为什么要使用双亲委派机制
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要让子 ClassLoader 再次加载一边,保证类的唯一性。
- 沙箱安全机制:防止开发人员恶意重写 JDK 原有的核心类,从而导致程序有很大的安全问题。