虽然在类加载的过程中,类加载器仅仅用于实现类的加载动作,但它在Java程序中起到的作用却远远超过类加载阶段
对于任意一个类,都必须由类本身和加载它的类加载器一起共同确立其在Java虚拟机中的唯一性(也就是说,虚拟机只有通过类和类加载器才能够唯一去确定类),每一个类加载器都拥有一个独立的类名称空间。
说得更通俗一点就是比较两个类是否相等,首先要确定加载这两个类的类加载器是不是一样的,如果类是由不一样的类加载器来加载的,即使两个类是来自同一个Class文件,被同一个虚拟机加载,都是被判定为不相等的,这里的相等关系是包括equals方法的返回结果、Instanceof的返回结果、isAssignableFrom的返回结果还有isInstance方法的返回结果
拓展:isAssignableFrom是Class实例里面的一个方法,用来判断对象所表示的类或接口与参数所表示的类或接口是不是相同的;isInstance也是Class实例里面的一个方法,用来判断指定obj对象是不是使用该类来进行实例的
这里的关键在于虚拟机是通过类和类加载器去唯一确定类的,假如使用不同的类加载去加载相同全限定名称的类,该类也会被判定成不相同的
从虚拟机的角度出发,只存在两种类加载器,一种是启动类加载器(BootStrap ClassLoader),对于HotSpot来说,启动类加载器是由C++语言来实现的,但可能对于其他厂家的虚拟机,比如MRP、Maxint等,整个虚拟机都是纯Java语言编写的;而另外一种则是其他类加载器,这些类加载器都是使用Java代码来进行实现的,独立于Java虚拟机,并且全都继承自抽象类java.lang.ClassLoader
但是从开发人员的角度来看的话,就应该划分得更加细致一些,从架构上来看,Java将类加载器划分成了两类
-
三层类加载器
-
双亲委派的类加载器
下面就看看这两种的类加载器是怎样的
常用的类加载器
首先我们要先认识一下,虚拟机常用的类加载器
绝大多数的Java程序都会使用到以下3个系统提供的类加载来进行加载
-
启动类加载器
-
扩展类加载器
-
应用程序类加载器
启动类加载器
启动类加载器是负责加载存放在java_home下的lib目录或者-Xbootclasspath参数所指定的路径所存放的jar,并且是Java虚拟机能够识别的类库,启动类加载器会将这些类库加载到虚拟机的内存中等待使用
启动类加载器是无法被应用程序直接使用的,如果我们在自定义类加载器的时候,如果需要把加载请求委派给引导类加载器去处理,最好的做法其实就是让自定义类加载的时候为null,就是不去定义类加载器。。。。。。感觉理解起来很绕口
直接看代码吧,下面是Class实例去获取自身类加载器的方法
从代码上看不出什么东西,所以我们从注释上去理解
注释上说明,该方法返回该class所使用的ClassLoader,一些类加载器实现类可能会使用null来表示bootstrap classLoader(启动类加载器),所以返回了getClassLoader方法返回为null,加旧代表使用的是启动类加载器
扩展类加载器
扩展类加载器是使用Java代码来实现的,并且是Launcher下面的一个内部类
ExtClassLoader负责加载lib目录下的ext路径的,并且从名字可以看出,这个是对于Java类库的一种扩展机制,因为该ExtClassLoader是使用Java来实现的,所以我们可以直接在应用程序上使用ExtClassLoader来加载Class文件
应用类加载器
这个应用类加载器与扩展类加载器一样,同样是使用Java语言来实现,同样是Launch类下面的内部类,同样也可以直接使用,名称为AppClassLoader,同时AppClassLoader被称为系统类加载器,这是因为ClassLoader.getSystemClassLoader的返回值就是它
应用类加载器负责加载用户类路径上所有的类库
这里先说明这个用户类路径,经过输出打印的结果如下
D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\deploy.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\javaws.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfxswt.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\management-agent.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\plugin.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar
D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar
D:\代码\leetcode\out\production\leetcode
可以看到类路径代表着lib、还有lib\ext下的,最重要的还有out文件夹下的,要知道out文件夹里面就是java项目里面编译出来的所有Class文件,所以应用类加载器主要就是负责加载我们自己写出来的Java代码编译出来的Class文件
双亲委派模型
下面我们直接看这个双亲委派模型是怎样的一个模型
可以看到,这个双亲委派模型跟继承关系十分类似,但其实两者并不是同种概念,要进行区分开来
我们可以看到,AppClassLoader并没有继承ExtClassLoader
但我们通过AppClassLoader的getParent方法去获取上层加载器并打印出来的时候,可以知道AppClassLoader的上层加载器就是ExtClassLoader
那么Java到底是如何实现这个双亲委派模型的呢???
双亲委派模型要求除了顶层的启动类服务器外,其余的类加载器都应有自己的父类加载器,不过这里的父类加载器并不是以继承的关系来实现的,而是使用组合的形式来实现的,从而复用父类的代码(这也是为什么在Launcher下没有看到AppClassLoader继承了ExtClassLoader),组合的形式也很简单,通过父加载器是子加载器的成员属性,这样就是组合了
但双亲委派模型只是推荐的一种形式,也就是说通常是这种模型,但其实双亲委派模型并不是一个具有强制约束力的模型,仅仅只是Java设计者们推荐给开发者的一种类加载器实现的最佳实践
双亲委派模型的工作过程
都说是双亲委派的,肯定要先考虑双亲呀
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会去加载这个类,而是把加载类的请求委派到父类加载器去完成,每一个层次的类加载器也是如此,如果父类加载器也有自己的父类加载器,那么也不会自己去加载,而是把加载类的请求委派到其父类加载器去完成,依次类推,导致的结果就是所有的加载请求的最终都会被传送到最顶层的启动类加载器中,只有当父类的加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成
那使用双亲委派模型有什么好处呢???
第一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系,正是这种优先级的层次关系保证了类加载出来都是一样的,前面提到过,类和其类加载器才能确定该类的唯一性,假如使用不同的类加载器去加载类,那么即使Class文件 相同,加载出来的结果也不一样,为了避免这种唯一性带来的不利,所以要给类加载器提供一种优先级层次,比如说,使用一个自定义的类加载去加载java.lang.Object,由于Object是由启动类加载器进行加载的(Object位于rt.jar中),所以根据双亲委派模型,最终不论哪种自定义的类加载器都会委派到启动类加载器进行加载,保证了Object类一定是由启动类加载器加载的,形成了一种绑定关系,那么Obejct类使用不同的自定义加载器去加载都能得到相同的结果
如果没有双亲委派模型,用户只要定义不同的自定义类加载器,去加载同一个全限定名的类,那么都会产生不同的结果
双亲委派模型对于保证Java程序的稳定运行极为重要,但它的实现却极为简单,下面就来看看这部分的代码,位于ClassLoader的loadClass方法上
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//检验是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没被加载,开始加载动作
long t0 = System.nanoT
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
ime();
try {
//判断有没有父加载器
//前面提到过,是使用组合的方式来实现双亲委派模型的
if (parent != null) {
//如果有父加载器,递归执行父加载器的loadClass方法
c = parent.loadClass(name, false);
} else {
//如果没有父类加载器
//代表是要由启动类加载器进行加载
//前面也提到过,如果要将类的加载委托给启动类加载器进行
//就将自定义的启动类的parent设置为Null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果上面的父加载器都加载失败
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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©;
}
return c;
}
}
逻辑还是很简单的
-
上锁
-
判断有没有父类加载器
-
有父类加载器,递归调用父类加载器的loadClass方法
-
没有父类加载器,说明要委托给启动类加载器来进行加载
-
如果父类加载器无法加载,返回依然为null,就会交由本类加载器来进行加载