一、概述
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器。
对于类加载,我们关注两个方面的问题:
- JVM定义的三个类加载器(Bootstrap,Extension,System)
- 双亲委派模型(机制,JDBC违背案例)
二、类加载器
JVM使用以下三种类型的类加载器:
-
启动类(Bootstrap)类加载器:
这个类存放在<JAVA_HOME>\lib目录中,无法被Java程序直接引用;
-
扩展(Extension)类加载器:
这个类加载器负责加载<JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用;
-
引用程序(Application)类加载器:
这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器,负责加载用户类路径上所指定的类库。开发者可以直接使用。
从java虚拟机的角度来说,一种是启动类加载器,是JVM的一部分;另一种是其他类加载器,独立于JVM,全部继承自抽象类java.lang.ClassLoader。只有其他类加载器程序员才能自己使用。
三、双亲委派模型
1.什么是双亲委派模型
JVM在加载类时默认采用的是双亲委派机制。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,然后父类加载器再委托给它的父类......以此类推,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。
只有父类加载器无法完成加载请求时,才会使用子类加载器去加载。
双亲委派模型的一个重要作用是为了保证类加载过程的安全性:
- 假设有一个开发者自己编写了一个名为
java.lang.Object
的类,想借此欺骗JVM。现在他要使用自定义ClassLoader
来加载自己编写的java.lang.Object
类。- 但是无论哪一个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,启动类加载器在其搜索范围内可以搜索到的只有
rt.jar
中的java.lang.Object
类- 这样就确保了Object类的唯一性,如果是String也是同理。
也因此,java判断类是否是同一个类,就是通过判断类的ClassLoader.class来判断的,举个简单的例子:只要不是一个爹妈生出来的,哪怕长的一模一样那也不是亲兄弟。
2.类的加载顺序
我们打开ClassLoader
类,找到loadClass()
方法,可以在注释上看到如何按顺序搜索类的:
* Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* Invoke the {@link #findClass(String)} method to find the
* class.
即:
- 使用
findLoadedClass()
方法检查类是否已经加载; - 调用父类加载器的
loaderCalss()
方法。如果父类是null,就直接调用虚拟机内置的Bootstrap加载; - 如果父类无法加载,使用自己
findClass()
加载。
我们再看一下loadClass()
方法的源码,更直观的展示了双亲委派模型下是如何加载类的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//线程安全,保证只有一个线程能对类初始化
synchronized (getClassLoadingLock(name)) {
//1.检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//2.如果没有被加载,判断是否存在父类加载器
if (parent != null) {
//3.有就递归委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//4.否则就使用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果到了启动类加载器还找不到类,则抛出ClassNotFoundException
}
//如果父类加载器无法加载,就使用自身的findClass加载
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这里就是为什么JDK在我们自定义类加载器的时候不推荐重写loadClass()
只让我们重写findClass()
方法的原因了,因为如果重写了方法,却不遵守双亲委派模型,就有可能导致不可预料的后果。
当然,不推荐归不推荐,如果有必要的话仍然可以重写loadClass()
方法,比如JDBC、JNDI就打破了双亲委派模型。
3.三种类加载器的关系
我们运行一下以下代码:
public static void main( String[] args ) {
System.out.println("应用程序加载器:"+ClassLoader.getSystemClassLoader());
System.out.println("应用程序加载器的父类:"+ClassLoader.getSystemClassLoader().getParent());
System.out.println("扩展加载器的父类:"+ClassLoader.getSystemClassLoader().getParent().getParent());
}
//输出结果
应用程序加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
应用程序加载器的父类:sun.misc.Launcher$ExtClassLoader@29453f44
扩展加载器的父类:null
由于启动类加载器是jvm内置的,我们无法直接调用,所以返回值是null。