文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
文章收录在网站:http://hardyfish.top/
类加载器
类加载器有什么作用呢?
对于任意的一个类,都必须由加载它的类加载器,这个类本身一起共同确立其在JVM中的唯一性
- 每一个类加载器,都拥有一个独立的类名称空间
换种说话就是:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义
- 否则,即使这两个类来源于同一个Class文件,被同一个JVM加载
- 只要加载它们的类加载器不同,这两个类也不相等
【注】:这里所指的相等,包括代表类的Class对象的equals()方法、
isAssignableFrom()
方法、isInstance()方法的返回结果
- 也包括了使用instanceof关键字做对象所属关系判定等各种情况
JVM中有几类类加载器呢?
其实从JVM的角度看,大致可以分为两类:
启动类加载器(
Bootstrap ClassLoader
):
- JVM自身的一部分,由C++实现的
- 只针对HotSpot虚拟机,JDK9以后也采用了类似的虚拟机与Java类互相配合来实现Bootstrap ClassLoader的方式
- 所以HotSpot也有一个无法获取实例的代表Bootstrap ClassLoader的Java类存在
其他所有的类加载器:
- 全部由Java实现,独立存在于VM外部,并全都继承自抽象类
java.lang.ClassLoader
【注】:从Java开发人员的角度来看,类加载器分的更细致
- 自JDK1.2以来,Java一直保持着三层类加载器,双亲委派的类加载器结构
类加载器双亲委派模型(Parents Delegation Model)
启动类加载器(
Bootstrap ClassLoader
):也叫引导类加载器,负责加载(存放在
<JAVA_HOME>\lib
目录,或者被Xbootclasspath参数所指定的路径中存放的
- 而且是JVM能够识别(按文件名识别,如
rt.jar、tools.jar
))的类库加载到JVM的内存中启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时
- 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可
扩展类加载器(Extension Class Loader):
- 在类
sun.misc.Launcher$ExtClassLoader
中以Java代码的形式实现的它负责加载
<JAVA_HOME>\lib\ext
目录中,或者被java.ext.dirs
系统变量所指定的路径中所有的类库通过名字我们可以推断出,JVM开发团队允许用户将自己的类库放置在
<JAVA_HOME>\lib\ext
目录中
- 以扩展Java SE的功能。因为是由Java语言实现的,所以我们可以直接在程序中使用该类加载器加载Class文件
【注】:在JDK9以后其实扩展机制已经可以被模块化代替了,因为模块化有天然的扩展能力
应用程序类加载器(Application Class Loader):
由
sun.misc.Launcher$AppClassLoader
来实现
- 该类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以我们也称其为系统类加载器
它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器
【注】:如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
自定义类加载器(User Class Loader):
JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的
当然我们也可以加入自定义的类加载器进扩展,比如:
增加存储于磁盘之外的Class文件来源;
实现类的隔离、重载等功能;
双亲委派模型的要求是什么?
顶层必须是启动类加载器
其余的类加载器都有自己的父类加载器;注意它们之间不是继承关系,而是通常使用组合(Composition)关系来复用父加载器的代码
双亲委派模型"的工作过程:
如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给父类加载器去完成,每一层次的类加载器都是如此
因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
双亲委派模型的好处:
Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系
例如类
java.lang.Object
,它存放在rt.jar
之中无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类
反之如果没有该模型,那么不同的类加载器可能会加载同一个名为
java.lang.Object
的类那么 在程序的classPath上就会有不同的Object类,那么我就不能保障Java类型体系了
双亲委派模型的核心代码: (全部集中在
java.lang.ClassLoader的loadClass()
方法之中)
protected synchronized Class<?> loadClass(String name, boolean resolve) throwsClassNotFoundException
{
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) { //如果未加载过就交给父类加载器
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载时,再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
逻辑解析:
检查请求加载的类是否已经被加载过
如果没有则调用父加载器的loadClass()方法
如果父加载器为空,则默认使用启动类加载器为父加载器
如果父加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法尝试进行加载
破坏双亲委派模型模型(一共三次):
第一次破坏:
因为JDK1.2以后双亲委派模型才被引入
但是在这之前类加载器的概念以及
java.lang.ClassLoader
就已经存在
- 所以面对当时已经存在的自定义类加载器,我们为了兼容它们,无法避免ClassLoader被子类覆盖的可能性
因此我们想到一个解决方法,就是在
java.lang.ClassLoader
中添加一个新的protected方法findClass()
- 并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码
(在双亲委派模型的核心代码中的第④步,这样就做到了妥协,如果父加载器加载失败,则会调用自己的findClass()方法
- 既按照用户意愿去加载类, 又保证符合双亲委派模型)
第二次破坏:
由自身缺陷导致,我们回想一下该模型的好处,无论哪一个类加载器要加载一个类,最终都是委派给处于模型最顶端的启动类加载器进行加载
如果该类加载加载失败,则抛出异常转而让下一层加载器加载(越基础的类由越上层的加载器进行加载)),这样解决了基础类型的不一致问题
但是有一个问题:
- 如果基础类型(被启动类加载器加载的)要调用用户的代码,那怎么办?
针对这个问题,我们引入了线程上下文类加载器(Thread Context ClassLoader)
该加载器可以通过
java.lang.Thread
类的setContext-ClassLoader()方法进行设置如果在创建线程的时候还没有创建,我们就通过它的父线程继承一个
- 如果其父线程甚至整个程序都没有设锅置,那我们就将应用类加载器充当线程上下文类加载器
而这种行为是父类加载器请求子类加载器去完成类加载,也就是变相的打破了双亲委派模型的层次结构逆向使用类加载器
- 已经违背了双亲委派模型的一般性原则
第三次破坏:
用户对程序动态性的追求而导致的。动态性指的是一些很热的名词,比如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
比如IBM公司的OSGi,实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(
OSGi
中称为Bundle)都有一个自己的类加载器当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换
在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构
它的查找顺序只有在开头符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行
OSGI在运行期动态热部署上的优势是JDK9以后
Sun/Oracle
公司所提出Jigsaw不能的
- 其只能局限于静态地解决模块间封装隔离和访问控制的问题