asda在上一篇文章中已经提到了类加载机制的"加载"阶段,一共分三个动作,第一个动作就是通过一个类的全限定名来获取描述该类的二进制流字节流。Java虚拟机设计团队有意把这个动作放到JVM外部去实现,以此让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为"类加载器"(Class Loader)。
类加载器
类加载器有什么作用呢?
as
asda对于任意的一个类,都必须由加载它的类加载器➕这个类本身一起共同确立其在JVM中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。
asda换种说话就是:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不同,这两个类也不相等。
ass【注】:这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况。
JVM中有几类类加载器呢?
as
d 其实从JVM的角度看,大致可以分为两类:
as
d s ①、启动类加载器(Bootstrap ClassLoader): JVM自身的一部分,由C++实现的。(只针对HotSpot虚拟机,JDK9以后也采用了类似的虚拟机与Java类互相配合来实现Bootstrap ClassLoader的方式,所以HotSpot也有一个无法获取实例的代表Bootstrap ClassLoader的Java类存在)
as
ads②、其他所有的类加载器: 全部由Java实现,独立存在于VM外部,并全都继承自抽象类java.lang.ClassLoader
as
dss【注】:从Java开发人员的角度来看,类加载器分的更细致。自JDK1.2以来,Java一直保持着三层类加载器,双亲委派的类加载器结构;
本篇文章主要针对 JDK 8 及之前版本的Java来介绍什么是三层类加载器,以及什么是双亲委派模型。
类加载器双亲委派模型(Parents Delegation Model)”
-
首先我们先看一下类加载器双亲委派模型的图:
-
启动类加载器(Bootstrap ClassLoader):
as
dssss也叫引导类加载器,负责加载(存放在<JAVA_HOME>\lib
目录,或者被Xbootclasspath
参数所指定的路径中存放的,而且是JVM能够识别(按文件名识别,如rt.jar、tools.jar))的类库加载到JVM的内存中。
as
dssss启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可. -
扩展类加载器(Extension Class Loader):
as
dssss在类sun.misc.Launcher$ExtClassLoader
中以Java代码的形式实现的;
as
dssss它负责加载<JAVA_HOME>\lib\ext
目录中,或者被java.ext.dirs
系统变量所指定的路径中所有的类库。
as
dssss通过名字我们可以推断出,JVM开发团队允许用户将自己的类库放置在<JAVA_HOME>\lib\ext
目录中, 以扩展Java SE的功能。因为是由Java语言实现的,所以我们可以直接在程序中使用该类加载器加载Class文件。
as
dssss【注】:在JDK9以后其实扩展机制已经可以被模块化代替了,因为模块化有天然的扩展能力。 -
应用程序类加载器(Application Class Loader):
as
dssss由sun.misc.Launcher$AppClassLoader
来实现;
as
dssss该类加载器是ClassLoader
类中的getSystem-ClassLoader()
方法的返回值,所以我们也称其为"系统类加载器";
as
dssss它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。
as
dssss【注】:如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 -
自定义类加载器(User Class Loader):
as
dssssJDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的。当然我们也可以加入自定义的类加载器进扩展。比如:
as
dssssss ①、增加存储于磁盘之外的Class文件来源;
as
dssssss ②、实现类的隔离、重载等功能; -
到底什么是"双亲委派模型呢"?
as
dssss我们把上图中各类加载器之间的关系称为类加载器的"双亲委派模型"。 -
双亲委派模型的要求是什么?
as
dssss①、顶层必须是启动类加载器;
as
dssss②、其余的类加载器都有自己的父类加载器;注意它们之间不是继承关系,而是通常使用组合(Composition)关系来复用父加载器的代码。 -
"双亲委派模型"的工作过程:
as
dssss如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给“父类”加载器去完成,每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
-
"双亲委派模型"的好处:
dssss
dssssJava中的类随着它的类加载器一起具备了一种带有优先级的层次关系;例如类java.lang.Object
,它存放在rt.jar
之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
dssss反之如果没有该模型,那么不同的类加载器可能会加载同一个名为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;
}
dssss逻辑解析:
dssasdasd ss ①、检查请求加载的类是否已经被加载过;
dssasdasd ss ②、如果没有则调用父加载器的loadClass()方法;
dssasdasd ss ③、如果父加载器为空,则默认使用启动类加载器为父加载器;
dssasdasd ss ④、如果父加载器失败,抛出ClassNotFoundException
异常后,再调用自己的findClass()
方法尝试进行加载。
- 破坏"双亲委派模型"模型(一共三次):
as
dssss第一次"破坏": 因为JDK1.2以后"双亲委派模型"才被引入,但是在这之前类加载器的概念以及java.lang.ClassLoader
就已经存在,所以面对当时已经存在的自定义类加载器,我们为了兼容它们,无法避免ClassLoader被子类覆盖的可能性。因此我们想到一个解决方法,就是在java.lang.ClassLoader
中添加一个新的protected
方法findClass()
,并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()
中编写代码。(在"双亲委派模型"的核心代码中的第④步,这样就做到了妥协,如果父加载器加载失败,则会调用自己的findClass()
方法,既按照用户意愿去加载类, 又保证符合"双亲委派模型")。
as
dssss第二次"破坏": 由自身缺陷导致,我们回想一下该模型的好处,无论哪一个类加载器要加载一个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,如果该类加载加载失败,则抛出异常转而让下一层加载器加载(越基础的类由越上层的加载器进行加载)),这样解决了基础类型的不一致问题。但是有一个问题:如果基础类型(被启动类加载器加载的)要调用用户的代码,那怎么办?
dssss针对这个问题,我们引入了线程上下文类加载器(Thread Context ClassLoader)。 该加载器可以通过java.lang.Thread
类的setContext-ClassLoader()
方法进行设置,如果在创建线程的时候还没有创建,我们就通过它的父线程继承一个。如果其父线程甚至整个程序都没有设锅置,那我们就将应用类加载器充当线程上下文类加载器。
dssss而这种行为是父类加载器请求子类加载器去完成类加载,也就是变相的打破了双亲委派模型的层次结构逆向使用类加载器,已经违背了双亲委派模型的一般性原则。
as
dssss第"三"次破坏: 用户对程序动态性的追求而导致的。动态性指的是一些很"热"的名词,比如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。
dssss比如IBM公司的OSGi,实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。 它的查找顺序只有在开头符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行。
dssssOSGI在运行期动态热部署上的优势是JDK9以后Sun/Oracle公司所提出Jigsaw不能的,其只能局限于静态地解决模块间封装隔离和访问控制的问题。
Java模块化系统(没耐心可以不看额,比较枯燥)
- JDK9引入的模块化系统是对Java技术的一次重要升级。
- 模块化的目标:可配置的封装隔离机制.
- 为了达到该目标,类加载器架构也有了相应的调整。
- 注意,Java模块定义要包括以下内容:
dssss①、首先是充当代码容器;
dssss②、依赖其他模块的列表(表明和其它模块的关系);
dssss③、开放的包列表,即其他模块可反射访问模块的列表;
dssss④、使用的服务列表;
dssss⑤、提供服务的实现列表; - dssssJDK9之前我们是基于 类路径(ClassPath)来查找依赖的可靠性问题。这时会有一个问题:如果类路径中缺失了运行时依赖的类型,那就只能等程序运行到发生该类型的加载、链接时才会报出运行的异常。
dssss而jdk9之后,我们是用 模块 进行封装,这样模块就可以声明对其他模块的显式依赖,Java虚拟机就能够在启动时验证应用程序开发阶段设定好的依赖关系在运行期是否完备,如有缺失那就直接启动失败,从而避免了很大一部分由于类型依赖而引发的运行时异常。
dssss【注】:为什么说很大一部分呢?因为模块化下也可能出现现ClassNotFoundExcepiton
异常,因为如果我们在某一个模块中把原本公开的包中的某些类移除,但不修改模块的导出信息,这样程序能顺利启动,但仍然会在运行期出现类加载异常。 - 兼容问题: 为了使可配置的封装隔离机制能够兼容传统的类路径查找机制,JDK 9提出了与“类路径”(ClassPath)相对应的“模块路径”(ModulePath)的概念;某个类库到底是传统的JAR包,还是模块取决于存放在哪个路径上,在类路径上就当作JAR包,反之当作模块。
- 传统程序为何可以访问到所有标准类库模块中导出的包(这是因为由有以下规则)?
dssssJAR文件在类路径的访问规则: 所有类路径下的JAR包都视为一个匿名模块,该模块几乎没有任何隔离,它可以看到和使用类路径上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有模块中导出的包。
dssss模块在模块路径的访问规则: 模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明依赖的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的 ,即具名模块看不见传统JAR包的内容。
dssssJAR文件在模块路径的访问规则: 传统的JAR文件放到模块路径上,会变成一个自动模块(Automatic Module),尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包。
dssss【注】:Java模块化系统目前不支持在模块定义中加入版本号来管理和约束依赖,本身也不支持多版本号的概念和版本选择功能。 如果同一个模块发行了多个不同的版本,那只能由开发者在编译打包时人工选择好正确版本的模块来保证依赖的正确性。 - 模块下的类加载器: 因为JDK1.2以后就有了"双亲委派模型",已有20多年,所有没有从根本上动摇该模型,只是为了模块化系统的顺利执行做了适当的调整:
dssss扩展类加载器: 被平台类加载器所取代,因为JDK以后,整个JDK都基于模块化构建,其中的Java类库就已天然地满足了可扩展的需求,所以不需要保留<JAVA_HOME>\lib\ext
目录。
dssss平台类加载器和应用程序类加载器: 都不再派生java.net.URLClassLoader
。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader
,在BuiltinClassLoader
中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理。 如图:
dssss【注】:在JDK9以后,有了"BootClassLoader“存在,也从侧面说明启动
类加载器现在是在Java虚拟机内部和Java类库共同协作实现的类加载器。尽管有了BootClassLoader这样的Java类,但为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如Object.class.getClassLoader())中仍然会返回null来代替,而不会得到BootClassLoader的实例。 - 在JDK9之后,虽然仍然维持着三层类加载器和双亲委派的架构,但类加载器的委派关系发生了改变:
dssss当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中, 如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载,也许这可以算是对双亲委派的第四次破坏。如图: