文章目录
- 一、双亲委派机制
- 二、双亲委派机制的作用
- 三、破坏双亲委派模型
- 四、双亲委派模型破坏举例JDBC
- 五、自定义类加载器
一、双亲委派机制
双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此(递归的去查找),只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。
显然,在介绍双亲委派机制的时候,不得不提ClassLoader。再说ClassLoader之前,我们得先了解下Java的基本知识。
Java是运行在Java的虚拟机(JVM)中的,但是它是怎么就运行在JVM中了呢?我们在IDE中编写的Java源代码被编译器编译成.class
的字节码文件。然后由我们的ClassLoader
负责将这些class文件加载到JVM
中去执行。
JVM中提供了三层的ClassLoader:
Bootstrap ClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),构造Extension ClassLoader
和Application ClassLoader
。
Extension ClassLoader(扩展类加载器):主要负责加载jre/lib/ext
目录下的一些扩展的jar。
Application ClassLoader(应用程序类加载器):主要负责加载应用程序
的主函数类。
最后一个Custom ClassLoader(用户自定义类加载器) 是java
编写,用户自定义的类加载器,可加载指定路径的class文件
。
那如果有一个Hello.class
文件是如何被加载到JVM中的呢?
我们简单看一下源码
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个class是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上述这段代码已经很好的解释了双亲委派机制,为了更容易理解,我们先参看下面描述上述代码流程的图再谈Hello.class
如何加载:
注:下图最上面为BootstrapClassLoader
从上图中我们就更容易理解了,当一个Hello.class
这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,直到到达Bootstrap ClassLoader之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层(其实就是递归查找过程),如果没有任何加载器能加载,就会抛出ClassNotFoundException
。
于是,我们就可以很好的总结双亲委派机制的工作流程了:
- 1、当
Application ClassLoader
收到一个类加载请求时,首先检查一下自己是否已经加载过,如果加载过就不用再加载了,没加载过也不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader
去完成。 - 2、当
Extension ClassLoader
收到一个类加载请求时,首先检查一下自己是否已经加载过,如果加载过就不用再加载了,没加载过也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader
去完成。 - 3、根加载器
Bootstrap ClassLoader
收到一个类加载请求时,首先检查一下自己是否已经加载过,如果加载过就不用再加载了,没加载过则去尝试加载这个类,如果Bootstrap ClassLoader
加载失败(在<JAVA_HOME>\lib
中未找到所需类),就会让Extension ClassLoader
尝试加载。 - 4、如果
Extension ClassLoader
也加载失败,就会使用Application ClassLoader
加载。 - 5、如果
Application ClassLoader
也加载失败,就会使用Custom ClassLoader
(用户自定义加载器)去尝试加载。 - 6、如果均加载失败,就会抛出
ClassNotFoundException
异常。
二、双亲委派机制的作用
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class对象。这样保证了Class执行安全。
举个栗子:如果有人想替换系统级别的类:String.java
。篡改它的实现,但是在这种机制下这些系统的类已经被Bootstrap ClassLoader
加载过了,所以并不会再去加载,从一定程度上防止了危险代码的植入。
三、破坏双亲委派模型
第一次破坏:
由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader
则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader
中添加一个新的protected
方法findClass()
,并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()
中编写代码。按照loadClass()
方法的逻辑,如果父类加载失败,会自动调用自己的findClass()
方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
第二次破坏:
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。
如果基础类又要调用回用户的代码,那该么办?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。
为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader
)。这个类加载器可以通过java.lang.Thread
类的setContextClassLoader()
方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
第三次破坏:
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。
四、双亲委派模型破坏举例JDBC
原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-.jar中的Driver类具体实现的。 原生的JDBC中的类是放在rt.jar包的,是由启动类加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,这个时候就引入线程上下文件类加载器(Thread Context ClassLoader)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
五、自定义类加载器
从上面loadClass源码中我们看出,按照loadClass()方法的逻辑,如果父类加载失败,会自动调用自己的findClass()方法来完成加载,这样既不影响用户按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则的。
因此,实现自定义的类加载器只需要继承自ClassLoader
然后重写它的findClass()方法即可。
自定义类加载器:
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
private String rootDir;/*自定义类加载的查找class的路径*/
/*指定该类加载器会查找的rootDir目录,和父加载器*/
public MyClassLoader(String rootDir, ClassLoader parent){
super(parent);
this.rootDir = rootDir;
}
/*指定该类加载器会查找的rootDir目录*/
public MyClassLoader(String rootDir){
this.rootDir = rootDir;
}
/**
* 自定义自己的类加载器,如没有要改变类加载顺序的必要的话,则重写findClass方法,因为这个方法是JDK预留了给我们实现的,
* 否则就需要修改loadClass的实现。
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//<1>.根据类的全路径(包含包名)类名和放置的目录确定类文件的路径
String className = name.substring(name.lastIndexOf(".")+1)+ ".class";
String classFile = rootDir + File.separator + className;
FileInputStream fileInputStream = null;
byte[] classData = null;
try {
//<2>.将class文件读取到字节数组
fileInputStream = new FileInputStream(new File(classFile));
classData = new byte[fileInputStream.available()];
fileInputStream.read(classData,0,classData.length);
//<3>.将字节数据创建一个class
return defineClass(name,classData,0,classData.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//<4>如果父类加载器不是自定义的,上面的加载过程没加载成功,则此调用会throw ClassNotFoundException
return super.findClass(name);
}
}
测试:(class路径和加载的类全限定类名自行更改,下面就是简单一个Person类,提供name和age属性以及他们的get、set方法)
public class CustomClassLoaderTest {
/*定义了一个目录存放class文件,这个其实可以修改为可配置参数*/
private static final String rootDir = "D:/class/";
public static void main(String[] args) throws Exception {
/*<1> 从指定的目录下查找对应的class文件,进行加载,然后创建该对象,如果加载存在则加载成功,则类加载器应为MyClassLoader*/
MyClassLoader classLoader = new MyClassLoader(rootDir);
Class c = classLoader.loadClass("com.dl.custom.Person");
Object object = c.newInstance();
Method getNameMethod = c.getMethod("getName");
Method getAgeMethod = c.getMethod("getAge");
System.out.println("name:" + getNameMethod.invoke(object) + ",age:" + getAgeMethod.invoke(object));
System.out.println("类加载器为:" + object.getClass().getClassLoader());
}
}
参考来源:
[1]、面试官:java双亲委派机制及作用 [2]、通俗易懂的双亲委派机制 [3]、什么是双亲委派机制? [4]、双亲委派模型和破坏性双亲委派模型详解 [5]、Java自定义类加载器实现-原理分析