虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码块被称为“类加载器”。
类加载器可以说是Java语言的一项创新,也是Java语言流行的重要原因之一,它最初是为了满足 Java Applet 的需求而开发出来的,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为Java技术体系的一块重要基石。
本博文接下来要介绍的知识点:
- 类加载器概念、作用
- 启动、扩展、应用程序类加载器的工作原理
- 双亲委派模式概念、工作原理、逻辑
- 自定义类加载器
- 源码探究双亲委派模式
JVM高级特性与实践(一):Java内存区域 与 内存溢出异常
JVM高级特性与实践(二):对象存活判定算法(引用) 与 回收
JVM高级特性与实践(三):垃圾收集算法 与 垃圾收集器实现
JVM高级特性与实践(四):内存分配 与 回收策略
JVM高级特性与实践(五):实例探究Class类文件 及 常量池
JVM高级特性与实践(六):Class类文件的结构(访问标志,索引、字段表、方法表、属性表集合)
JVM高级特性与实践(七):九大类字节码指令集(实例探究 )
JVM高级特性与实践(八):虚拟机的类加载机制
JVM高级特性与实践(十):虚拟机字节码执行引擎(栈帧结构)
JVM高级特性与实践(十一):方法调用 与 字节码解释执行引擎(实例解析)
JVM高级特性与实践(十二):Java内存模型 与 高效并发时的内外存交互(volatile变量规则)
JVM高级特性与实践(十三):线程实现 与 Java线程调度
JVM高级特性与实践(十四):线程安全 与 锁优化
一. 类与类加载器
1. 类加载器作用
类加载器虽然只用于实现类的加载动作,但它在Java程序起到的作用却远大于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”,只有在这两个类时由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
2. 类加载器 与 instanceof 关键字
上文所说的“相等”,包括代表类的,对象的 equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果,以下代码演示了不同的类加载器对 instanceof 关键字运算结果的影响:
/**
* 类加载器与instanceof关键字演示
*
* @author zzm
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("org.fenixsoft.classloading.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
}
}
运行结果:
分析
以上代码中构造了一个简单的类加载器,它可以加载与自己在同一路径下的Class文件。使用这个类加载器去加载一个名为“org.fenixsoft.classloading.ClassLoaderTest”的类,并实例化这个类的对象。
从输出结果的第一行可以看出,此对象确实是类“org.fenixsoft.classloading.ClassLoaderTest”实例化出的对象,但从第二句看出,此对象与类“org.fenixsoft.classloading.ClassLoaderTest”做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个 ClassLoaderTest 类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然返回 false。
二. 双亲委派模式
1. 虚拟机角度中的类加载器
从虚拟机的角度来讲,只存在两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):此类加载器使用C++实现,是虚拟机自身的一部分。
- 所有其他类加载器:由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
2. Java开发人员角度中的类加载器
从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器:
(1) 启动类加载器(Bootstrap ClassLoader)
此类加载器负责将存放在 <JAVA_HOME>\lib
目录中的,或者被 -Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在lib 目录中也不会被加载)类库加载到虚拟机内存中。
启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。如下代码java.lang.ClassLoader.getClassLoader()
方法的代码片段:
【ClassLoader.getClassLoader() 方法的代码片段】
/**
* Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.
*/
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader cc1 = ClassLoader.getCallerClassLoader();
if(cc1 != null && cc1 != c1 && !c1.isAncestor(cc1)){
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
return cl;
}
(2)扩展类加载器(Extension ClassLoader)
是由ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将Java_Home /lib/ext
或者由系统变量 java.ext.dir
指定位置中的类库加载到内存中,开发者可以直接使用标准扩展类加载器。如下图示例:
(3)应用程序类加载器(Application ClassLoader)
是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader()
方法的返回值,因此一般称为系统类加载器。
它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
3. 双亲委派模型
(1)概念
我们的应用程序都是由这3 种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系如下图:
上图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。
类加载器的双亲委派模型在JDK 1.2期间被引入并被广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。
(2)工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
(3)模式优点
使用双亲委派模型来组织类加载器之间的关系,好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,用户编写了一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但是永远无法被加载运行。
(4)双亲委派模型的系统实现
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却很简单,实现集中在java.lang.ClassLoader的loadClass()
方法中,在其方法中,主要判断逻辑如下:先检查是否已经被加载过,
- 若没有被加载过,则接着判断父加载器是否为空。
- 若不为空,则调用父类加载器的
loadClass()
方法。 - 若父加载器为空,则默认使用启动类加载器作为父加载器。
- 若不为空,则调用父类加载器的
- 如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的
findClass()
方法进行加载。
【双亲委派模型的实现】
protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
//check the class has been loaded or not
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//if throws the exception ,the father can not complete the load
}
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
(5)注意(findClass方法)
在查看学习以上ClassLoader的实现后,注意一个地方,即“如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()
方法进行加载”这一步逻辑,进一步查看findClass()
方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
没错!此方法没有具体实现,只是抛了一个异常,而且访问权限是protected。这充分证明了:这个方法就是给开发者重写用的,即自定义类加载器时需实现此方法!
4. 破坏双亲委派模型
双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器的实现方式。大多数的类加载器都遵循这个模型,但是JDK中也有3次较大规模破坏双亲模型的情况,以下只着重介绍第一次“被破坏”,与后续的讲解有关。
双亲委派模型的第一次“被破坏”发生在此模型出现之前——JDK1.2发布前,此模型在JDK 1.2之后才引入,而类加载器和抽象类 java.lang.ClassLoader 添加了一个新的 protected限定的findClass()
方法,而在此之前,用户去继承 java.lang.ClassLoader 的唯一目的是重写 loadClass()
方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法 loadClassInternal()
,此方法唯一逻辑就是去调用自己的loadClass()
方法。
☆☆☆☆☆
在上一点中已经介绍过loadClass()
方法的代码,而双亲委派模型的具体逻辑也在此方法中,在JDK 1.2之后已不提倡用户再去覆盖loadClass()
方法,而应当把自己的类加载逻辑写到 findClass()
方法中,在loadClass()
方法逻辑里:如果父类加载失败,则会调用自己的 findClass()
方法来完成加载,这样可以保证用户自定义的类加载器时符合双亲委派规则的!
三. 实例验证双亲委派模式
在了解了以上多种类加载器和双亲委派模式的理论实现后,对这个“双亲委派模式”运作,自我感觉还是有点迷糊,所以接下来自定义一个类加载器来亲自测试验证“双亲委派模式”的运行实现,从实践角度出发来一探究竟。
在了解完以上双亲委派模型、破坏双亲委派模型后,我们得知自定义类加载器有以下两种方式:
采用双亲委派模型:只需要重写ClassLoader的
findClass()
方法即可破坏双亲委派模型:重写ClassLoader的整个
loadClass()
方法(因为双亲委派模型的逻辑主要实现就在此方法中,若我们重写即可破坏掉。)
不过此次实践就是为了来探究验证双亲委派模型,多以我们当然是采取第一种方法来自定义类加载器。
1. 自定义类加载器
首先第一步我们需要自定义一个简单实现的类加载器,通过第二大点最后的讲解后对自定义类加载器的过程稍有了解,构建重点:自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只需实现findClass()
方法即可。
注意:此类里面主要是一些IO和NIO操作,其中defineClass
方法可以把二进制流字节组成的文件转换为一个java.lang.Class,只要二进制字节流的内容符合Class文件规范即可。
/*
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
}
public MyClassLoader(ClassLoader parent){
super(parent);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
//打印日志,表示使用的是自定义的类加载器
System.out.println("Use myclassloader findClass method.");
//获取的fileName为: EasyTest.class
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
byte[] bytes = loadClassData("E:\\test_eclipse\\JvmProject\\"+fileName);
return defineClass(name, bytes, 0, bytes.length);
}
public byte[] loadClassData(String name) {
//这里参数name路径为:E:\test_eclipse\JvmProject\EasyTest.class
FileInputStream fileInput = null;
ByteArrayOutputStream bytesOutput = null;
try {
fileInput = new FileInputStream(new File(name));
bytesOutput = new ByteArrayOutputStream();
int b = 0;
while ((b = fileInput.read()) != -1) {
bytesOutput.write(b);
}
return bytesOutput.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(fileInput != null)
fileInput.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
注意:此时是使用MyClassLoader 自定义加载器来加载 EasyTest类(空实现,只是一个空壳),需要将该类编译后生成的EasyTest.class文件放到E:\test_eclipse\JvmProject\
路径中,而测试的main
方法如下:
/*
* 测试自定义加载器的Main方法
*/
public static void main(String[] args){
MyClassLoader myClassLoader = new MyClassLoader();
try {
Class<? extends Object> testClass = myClassLoader.loadClass("org.fenixsoft.classloading.EasyTest");
Object obj = testClass.newInstance();
System.out.println(obj.getClass().getClassLoader().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
结果分析
查看上图红框显示结果可得,虽然程序的确使用自定义类加载器加载的,可是显示并非是 MyClassLoader而是应用程序类加载器 AppClassLoader 加载的,而这种结果的造成原因正是因为 双亲委派模式。后面通过实例来探究双亲委派模式的源码实现。
2. 源码探究
回顾一下在讲解双亲委派模式中的相关知识点及层次图,启动类加载器只是JVM的一个类加载工具,处于层次图的最上层,严格来说它并无遵守此模式,所以我们从它的下层 —— 扩展类加载器和应用程序类加载器开始分析。
查看其输出结果可知,其中涉及到了 sun.misc.Launcher类,查看此类结构图:
(1)sun.misc.Launcher类构造方法
点进去 sun.misc.Launcher类进行查看,你会发现在它的构造方法中创建了扩展类加载器 ExtClassLoader的实例,并用该实例创建应用程序类加载器 AppClassLoader的实例:
public Launcher() {
// 创建扩展类加载器
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError("Could not create extension class loader");
}
// 通过扩展类加载器来创建应用程序类加载器
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader");
}
......
}
(2)扩展类加载器的实例化
由上可知,在Launcher类构造方法中先实例化了扩展类加载器,查看其实现过程:
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException{
final File[] dirs = getExtDirs();
......
return new ExtClassLoader(dirs);
......
}
private static File[] getExtDirs() {
// 加载路径
String s = System.getProperty("java.ext.dirs");
......
}
public ExtClassLoader(File[] dirs) throws IOException {
// 这里的第二个参数含义是指定上级类加载器
super(getExtURLs(dirs), null, factory);
this.dirs = dirs;
}
......
}
注意:查看ExtClassLoader 的构造方法中,调用了父类构造方法,其中传入的第二个参数为null,代表扩展类加载器没有上级类加载器!
(3)应用程序类加载器的实例化
紧接着来看应用程序类加载器的实例化过程:
static class AppClassLoader extends URLClassLoader {
// extcl是ExtClassLoader类的实例
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
......
return new AppClassLoader(urls, extcl);
......
}
AppClassLoader(URL[] urls, ClassLoader parent) {
// 应用程序类加载器的上级是扩展类加载器
super(urls, parent, factory);
}
......
}
注意:查看AppClassLoader 的构造方法中,调用了父类构造方法,其中传入的第二个参数为parent,也就是扩展类加载器extcl,直接从代码的角度证明 扩展类加载器是应用程序加载器的上级!
扩展类加载器是应用程序加载器的上级(已证明)
(4)ClassLoader的构造方法
在我们创建自定义类加载器时,继承了ClassLoader类,所以程序在运行时会先调用父类——ClassLoader的构造函数,来查看其实现:
private ClassLoader parent;
private static ClassLoader scl;
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
public static ClassLoader getSystemClassLoader() {
// scl在改方法中创建
initSystemClassLoader();
.....,
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
// 获取Launcher类的实例
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
// 参照前面Launcher类的构造函数,scl就是应用程序类加载器
scl = l.getClassLoader();
......
sclSet = true;
}
}
}
由以上可证明一个级别关系:
应用程序类加载器是自定义类加载器的上级。
我们在之前介绍过双亲委派模式的工作原理,通过前面一系列的分析后,再次叙述一遍,感受更深:如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给上级类加载器去完成,每一个层次的类加载器都是如此,所有加载器的加载请求最终都应该传送至最顶层的类加载器中(扩展类加载器),只有当上级类加载器反馈自己无法完成这个加载请求(它的类加载范围中没有找到所需的类)时,下级类加载器才会去尝试自己加载这个类。
由以上一系列源码探究可窥得这双亲委派模式的工作原理,并且清楚了为何最终加载 EasyTest类的是应用程序类加载器而并非是我们自定义的类加载器。
3. 使用自定义类加载器 加载
如今以上的道理算是理解了,可现在偏偏需要使用自定义的类加载器加载,应该如何修改呢?
在测试main()
方法中创建自定义类加载器的代码:
MyClassLoader myClassLoader = new MyClassLoader();
修改成:
MyClassLoader myClassLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());
即把自定义ClassLoader的父加载器设置为扩展类加载器,这样父加载器加载不到EasyTest.class文件,就交由子加载器MyClassLoader来加载了(别忘了在自定义类加载器中要写对应的构造方法)。
运行结果如下:
4. .class和getClass()
这两者看起来类似,但其实有很大区别,如下:
- .class用于类名,
getClass()
是一个final native的方法,因此用于类实例。 - .class在编译期间就确定了一个类的java.lang.Class对象,但是
getClass()
方法在运行期间确定一个类实例的java.lang.Class对象
5. ClassLoader.getResourceAsStream(String name)
不知道细心的朋友有没有注意到此方法,在本博文的第一个例子中出现过。在我们自定义的类加载器中所占篇幅最大的就是一个loadClassData
方法,将文件数据转换为字节数组,但是还有第二种方法,就是采用系统提供的 ClassLoader.getResourceAsStream(String name)
方法,根据此方法获取到数据输入流,通过此输入流获取字节数组,最终传入defineClass
方法即可,代码如下:
【自定义类加载器中的 findClass方法】
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
此方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称是以”/”分隔的标识资源名称的路径名称。有兴趣可查看其源码,实现也很简单。
四. 总结
文章总结
本篇博文的内容并不少,首先第一点初步介绍了类加载器的概念、作用,用一个简单实现的类加载器来证明两个类之间的“相等”比较。接着在第二点中详细介绍了三种不同的类加载器,并学习了双亲委派模式,将启动、扩展、应用程序类加载器的层次归纳到双亲委派模型中,理解了此模型的优点、工作原理、源码逻辑等。
如果说以上两点比较偏理论的话,那么在第三点中采用自定义类加载器实例来探究双亲委派模式的工作原理,从源码的角度来分析ClassLoader的实现及类加载情况,此部分尤为重要。
作者有感
这篇博文其实我在写完前两大点就准备发布了,可当我写完之后回顾了一下所学知识点,突然发现自己只是有个大致的概念,但是依旧迷迷糊糊的,说是“深入理解”,我真的深入了吗?融汇贯通了吗?并没有。于是我试着自己写一个自定义加载器来验证双亲委派模式,果不其然,如上问题出现了,却发现想不明白,于是查找学习其它学者写的博客(感谢各位学者的乐于分享知识精神)。学习之后却恍然大悟,我之前只是照本宣科,他们却已融汇贯通、举一反三、深入进行源码验证,深感羞愧,最后慢慢总结出这第三点来,在这过程中反复思考,才是真正学到了。
实践是检验真理的唯一标准~
若有错误,欢迎指教~