本文曾经收录进 JavaGuide(「Java 学习 + 面试指南」一份涵盖大部分 Java 程序员所须要把握的外围常识。)
加入过校招面试的同学,应该对这个问题不生疏。个别发问 JVM 知识点的时候,就会顺带问你双亲委派模型(顺当的翻译。。。)。
就算是不筹备面试,学习双亲委派模型对于咱们也十分有帮忙。咱们比拟相熟的 Tomcat 服务器为了实现 Web 利用的隔离,就自定义了类加载并突破了双亲委派模型。
这篇文章我会先介绍类加载器,再介绍双亲委派模型,这样有助于咱们更好地了解。
目录概览:
回顾一下类加载过程
开始介绍类加载器和双亲委派模型之前,简略回顾一下类加载过程。
- 类加载过程:加载 -> 连贯 -> 初始化。
- 连贯过程又可分为三步:验证 -> 筹备 -> 解析。
加载是类加载过程的第一步,次要实现上面 3 件事件:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的动态存储构造转换为办法区的运行时数据结构
- 在内存中生成一个代表该类的
Class
对象,作为办法区这些数据的拜访入口
类加载器
类加载器介绍
类加载器从 JDK 1.0 就呈现了,最后只是为了满足 Java Applet(曾经被淘汰)的须要。起初,缓缓成为 Java 程序中的一个重要组成部分,赋予了 Java 类能够被动静加载到 JVM 中并执行的能力。
依据官网 API 文档的介绍:
A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.
Every Class object contains a reference to the ClassLoader that defined it.
Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.
翻译过去大略的意思是:
类加载器是一个负责加载类的对象。
ClassLoader
是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成形成类定义的数据。典型的策略是将名称转换为文件名,而后从文件系统中读取该名称的“类文件”。每个 Java 类都有一个援用指向加载它的
ClassLoader
。不过,数组类不是通过ClassLoader
创立的,而是 JVM 在须要的时候主动创立的,数组类通过getClassLoader()
办法获取ClassLoader
的时候和该数组的元素类型的ClassLoader
是统一的。
从下面的介绍能够看出:
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个援用指向加载它的
ClassLoader
。 - 数组类不是通过
ClassLoader
创立的(数组类没有对应的二进制字节流),是由 JVM 间接生成的。
class Class<T> {
...
private final ClassLoader classLoader;
@CallerSensitive
public ClassLoader getClassLoader() {//...}
...
}
简略来说,类加载器的次要作用就是加载 Java 类的字节码(.class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。 字节码能够是 Java 源程序(.java
文件)通过 javac
编译得来,也能够是通过工具动静生成或者通过网络下载得来。
其实除了加载类之外,类加载器还能够加载 Java 利用所需的资源如文本、图像、配置文件、视频等等文件资源。本文只探讨其外围性能:加载类。
类加载器加载规定
JVM 启动的时候,并不会一次性加载所有的类,而是依据须要去动静加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加敌对。
对于曾经加载的类会被放在 ClassLoader
中。在类加载的时候,零碎会首先判断以后类是否被加载过。曾经被加载的类会间接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,雷同二进制名称的类只会被加载一次。
public abstract class ClassLoader {
...
private final ClassLoader parent;
// 由这个类加载器加载的类。private final Vector<Class<?>> classes = new Vector<>();
// 由 VM 调用,用此类加载器记录每个已加载类。void addClass(Class<?> c) {classes.addElement(c);
}
...
}
类加载器总结
JVM 中内置了三个重要的 ClassLoader
:
BootstrapClassLoader
(启动类加载器):最顶层的加载类,由 C++ 实现,通常示意为 null,并且没有父级,次要用来加载 JDK 外部的外围类库(%JAVA_HOME%/lib
目录下的rt.jar
、resources.jar
、charsets.jar
等 jar 包和类)以及被-Xbootclasspath
参数指定的门路下的所有类。ExtensionClassLoader
(扩大类加载器):次要负责加载%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
零碎变量所指定的门路下的所有类。AppClassLoader
(应用程序类加载器):面向咱们用户的加载器,负责加载以后利用 classpath 下的所有 jar 包和类。
🌈 拓展一下:
rt.jar
:rt 代表“RunTime”,rt.jar
是 Java 根底类库,蕴含 Java doc 外面看到的所有的类的类文件。也就是说,咱们罕用内置库java.xxx.*
都在外面,比方java.util.*
、java.io.*
、java.nio.*
、java.lang.*
、java.sql.*
、java.math.*
。- Java 9 引入了模块零碎,并且稍微更改了上述的类加载器。扩大类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个要害模块,比如说
java.base
是由启动类加载器加载之外,其余的模块均由平台类加载器所加载。
除了这三品种加载器之外,用户还能够退出自定义的类加载器来进行拓展,以满足本人的非凡需要。就比如说,咱们能够对 Java 类的字节码(.class
文件)进行加密,加载时再利用自定义的类加载器对其解密。
除了 BootstrapClassLoader
是 JVM 本身的一部分之外,其余所有的类加载器都是在 JVM 内部实现的,并且全都继承自 ClassLoader
抽象类。这样做的益处是用户能够自定义类加载器,以便让应用程序本人决定如何去获取所需的类。
每个 ClassLoader
能够通过 getParent()
获取其父 ClassLoader
,如果获取到 ClassLoader
为 null
的话,那么该类是通过 BootstrapClassLoader
加载的。
public abstract class ClassLoader {
...
// 父加载器
private final ClassLoader parent;
@CallerSensitive
public final ClassLoader getParent() {//...}
...
}
为什么 获取到 ClassLoader
为 null
就是 BootstrapClassLoader
加载的呢? 这是因为BootstrapClassLoader
由 C++ 实现,因为这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的后果是 null。
上面咱们来看一个获取 ClassLoader
的小案例:
public class PrintClassLoaderTree {public static void main(String[] args) {ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue){System.out.println(split.toString() + classLoader);
if(classLoader == null){needContinue = false;}else{classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
输入后果(JDK 8):
|--sun.misc.Launcher$AppClassLoader@18b4aac2
|--sun.misc.Launcher$ExtClassLoader@53bd815b
|--null
从输入后果能够看出:
- 咱们编写的 Java 类
PrintClassLoaderTree
的ClassLoader
是AppClassLoader
; AppClassLoader
的父ClassLoader
是ExtClassLoader
;ExtClassLoader
的父ClassLoader
是Bootstrap ClassLoader
,因而输入后果为 null。
自定义类加载器
咱们后面也说说了,除了 BootstrapClassLoader
其余类加载器均由 Java 实现且全副继承自 java.lang.ClassLoader
。如果咱们要自定义本人的类加载器,很显著须要继承 ClassLoader
抽象类。
ClassLoader
类有两个要害的办法:
protected Class loadClass(String name, boolean resolve)
:加载指定二进制名称的类,实现了双亲委派机制。name
为类的二进制名称,resove
如果为 true,在加载时调用resolveClass(Class<?> c)
办法解析该类。protected Class findClass(String name)
:依据类的二进制名称来查找类,默认实现是空办法。
官网 API 文档中写到:
Subclasses of
ClassLoader
are encouraged to overridefindClass(String name)
, rather than this method.倡议
ClassLoader
的子类重写findClass(String name)
办法而不是loadClass(String name, boolean resolve)
办法。
如果咱们不想突破双亲委派模型,就重写 ClassLoader
类中的 findClass()
办法即可,无奈被父类加载器加载的类最终会通过这个办法被加载。然而,如果想突破双亲委派模型则须要重写 loadClass()
办法。
双亲委派模型
双亲委派模型介绍
类加载器有很多种,当咱们想要加载一个类的时候,具体是哪个类加载器加载呢?这就须要提到双亲委派模型了。
依据官网介绍:
The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. The virtual machine’s built-in class loader, called the “bootstrap class loader”, does not itself have a parent but may serve as the parent of a ClassLoader instance.
翻译过去大略的意思是:
ClassLoader
类应用委托模型来搜寻类和资源。每个ClassLoader
实例都有一个相干的父类加载器。须要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜寻类或资源的工作委托给其父类加载器。
虚拟机中被称为 “bootstrap class loader” 的内置类加载器自身没有父类加载器,然而能够作为ClassLoader
实例的父类加载器。
从下面的介绍能够看出:
ClassLoader
类应用委托模型来搜寻类和资源。- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有本人的父类加载器。
ClassLoader
实例会在试图亲自查找类或资源之前,将搜寻类或资源的工作委托给其父类加载器。
下图展现的各种类加载器之间的档次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
留神⚠️:双亲委派模型并不是一种强制性的束缚,只是 JDK 官网举荐的一种形式。如果咱们因为某些非凡需要想要突破双亲委派模型,也是能够的,后文会介绍具体的办法。
其实这个双亲翻译的容易让他人误会,咱们个别了解的双亲都是父母,这里的双亲更多地表白的是“父母这一辈”的人而已,并不是说真的有一个 MotherClassLoader
和一个FatherClassLoader
。集体感觉翻译成单亲委派模型更好一些,不过,国内既然翻译成了双亲委派模型并流传了,依照这个来也没问题,不要被误会了就好。
另外,类加载器之间的父子关系个别不是以继承的关系来实现的,而是通常应用组合关系来复用父加载器的代码。
public abstract class ClassLoader {
...
// 组合
private final ClassLoader parent;
protected ClassLoader(ClassLoader parent) {this(checkCreateClassLoader(), parent);
}
...
}
在面向对象编程中,有一条十分经典的设计准则:组合优于继承,多用组合少用继承。
双亲委派模型的执行流程
双亲委派模型的实现代码非常简单,逻辑十分清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相干代码如下所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{synchronized (getClassLoadingLock(name)) {
// 首先,查看该类是否曾经加载过
Class c = findLoadedClass(name);
if (c == null) {
// 如果 c 为 null,则阐明该类没有被加载过
long t0 = System.nanoTime();
try {if (parent != null) {
// 当父类的加载器不为空,则通过父类的 loadClass 来加载该类
c = parent.loadClass(name, false);
} else {
// 当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {// 非空父类的类加载器无奈找到相应的类,则抛出异样}
if (c == null) {
// 当父类加载器无奈加载时,则调用 findClass 办法来加载该类
// 用户可通过覆写该办法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
// 用于统计类加载器相干的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
// 对类进行 link 操作
resolveClass(c);
}
return c;
}
}
每当一个类加载器接管到加载申请时,它会先将申请转发给父类加载器。在父类加载器没有找到所申请的类的状况下,该类加载器才会尝试去加载。
联合下面的源码,简略总结一下双亲委派模型的执行流程:
- 在类加载的时候,零碎会首先判断以后类是否被加载过。曾经被加载的类会间接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现(调用父加载器
loadClass()
办法来加载类)。这样的话,所有的申请最终都会传送到顶层的启动类加载器BootstrapClassLoader
中。 - 只有当父加载器反馈本人无奈实现这个加载申请(它的搜寻范畴中没有找到所需的类)时,子加载器才会尝试本人去加载(调用本人的
findClass()
办法来加载类)。
🌈 拓展一下:
JVM 断定两个 Java 类是否雷同的具体规定:JVM 不仅要看类的全名是否雷同,还要看加载此类的类加载器是否一样。只有两者都雷同的状况,才认为两个类是雷同的。即便两个类来源于同一个 Class
文件,被同一个虚拟机加载,只有加载它们的类加载器不同,那这两个类就必然不雷同。
双亲委派模型的益处
双亲委派模型保障了 Java 程序的稳固运行,能够防止类的反复加载(JVM 辨别不同类的形式不仅仅依据类名,雷同的类文件被不同的类加载器加载产生的是两个不同的类),也保障了 Java 的外围 API 不被篡改。
如果没有应用双亲委派模型,而是每个类加载器加载本人的话就会呈现一些问题,比方咱们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,零碎就会呈现两个不同的 Object
类。双亲委派模型能够保障加载的是 JRE 里的那个 Object
类,而不是你写的 Object
类。这是因为 AppClassLoader
在加载你的 Object
类时,会委托给 ExtClassLoader
去加载,而 ExtClassLoader
又会委托给 BootstrapClassLoader
,BootstrapClassLoader
发现自己曾经加载过了 Object
类,会间接返回,不会去加载你写的 Object
类。
突破双亲委派模型办法
为了防止双亲委托机制,咱们能够本人定义一个类加载器,而后重写 loadClass()
即可。
🐛 修改(参见:issue871):自定义加载器的话,须要继承 ClassLoader
。如果咱们不想突破双亲委派模型,就重写 ClassLoader
类中的 findClass()
办法即可,无奈被父类加载器加载的类最终会通过这个办法被加载。然而,如果想突破双亲委派模型则须要重写 loadClass()
办法。
为什么是重写 loadClass()
办法突破双亲委派模型呢?双亲委派模型的执行流程曾经解释了:
类加载器在进行类加载的时候,它首先不会本人去尝试加载这个类,而是把这个申请委派给父类加载器去实现(调用父加载器
loadClass()
办法来加载类)。
咱们比拟相熟的 Tomcat 服务器为了可能优先加载 Web 利用目录下的类,而后再加载其余目录下的类,就自定义了类加载器 WebAppClassLoader
来突破双亲委托机制。这也是 Tomcat 下 Web 利用之间的类实现隔离的具体原理。
Tomcat 的类加载器的层次结构如下:
感兴趣的小伙伴能够自行钻研一下 Tomcat 类加载器的层次结构,这有助于咱们搞懂 Tomcat 隔离 Web 利用的原理,举荐材料是《深刻拆解 Tomcat & Jetty》。
举荐浏览
- 《深刻拆解 Java 虚拟机》
- 深入分析 Java ClassLoader 原理:https://blog.csdn.net/xyang81/article/details/7292380
- Java 类加载器(ClassLoader):http://gityuan.com/2016/01/24/java-classloader/
- Class Loaders in Java:https://www.baeldung.com/java-classloaders
- Class ClassLoader – Oracle 官网文档:https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoad…
- 老大难的 Java ClassLoader 再不了解就老了:https://zhuanlan.zhihu.com/p/51374915