双亲委派机制概述
- Java虚拟机对class文件采用按需加载的方式,也就是说当需要使用该类时,才会将它的class文件加载到内存生成class对象。而且加载某个类的文件是,采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
- 先看下面的一个例子
新建一个 java.lang 包,在下面自定义一个String类 ,如图
然后在Test中创建String类
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
String string = new String();
java.lang.String s2 = new java.lang.String();
System.out.println("hello");
}
}
如果创建的对象是我们自定义的类则会输出静态代码块中的 ““我是自定义 String …””,否则不会。
这里结果指定是不会打印的,这就是双亲委派机制的作用。
双亲委派机制原理
- 如果一个类加载器接收到了类加载请求,它并不会自己先去加载,而是把这个类加载委托给父类加载器去执行。
- 如果父类加载器还存在父加载器,则进一步向上委托,依次递归,请求将最终达到顶层的启动类加载器。
- 如果父类可以完成类加载任务,就成功返回,若父类无法完成类加载任务,子加载器才会尝试去加载,这就是双亲委派模式。
- 如果将上面的代码 main方法放在自定义的 String 类中
public class String {
static {
System.out.println("我是自定义 String ...");
}
public static void main(String[] args) {
System.out.println("main....");
}
}
执行的时候报错如下:
意思就是加载的 java.lang.String 类中没有 main方法,也证明了之前的结论。
双亲委派机制的优势
- 避免类重复加载
- 保护程序安全,避免核心API被随意篡改。(如上面的自定义 java.lang.String 类)也包括阻止我们使用和核心api相同的包名来自定义类。
如下图
报错如下:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at Test.main(Test.java:5)
沙箱安全机制
- 自定义String类,但是在加载自定义String类时会率先使用引导类加载器加载,而引导类加载器在加载过程中会优先加载jdk自带的文件,上面例子报错信息说没有 main 方法,就是因为加载的是 jdk的 java.lang包下的String类。这样可以保证java源码的保护,这就是沙箱安全机制。
破坏双亲委派机制
- 双亲委派机制模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者们的类加载实现方式。在 Java 的世界中,大部分类加载都遵循了这个模型,但也有例外的情况。直到Java模块化出现,双亲委派机制已经有三次较大规模“被破坏”的情况。
- 双亲委派模型第一次被破坏:
双亲委派模型第一次被破坏发生在双亲委派模型出现之前,即jdk1.2 之前。由于双亲委派模型在jdk1.2以后才被引入,但是类加载的概念和抽象类 ClassLoader 已经存在,面对已经存在的用户自定义类加载器代码,Java设计者们引入双亲委派模型不得不作出一些妥协,为了兼容这些已有代码,无法再以技术手段避免 loadClass() 被自类覆盖的可能,只能在 jdk1.2以后的java.lang.ClassLoader 中添加一个新的 protected 方法 findClass() ,并引导用户编写的类加载器逻辑尽可能去重写这个方法,而不是在 loadClass() 中编写代码。loadClass() 方法中,双亲委派的具体逻辑就写在这里面,按照loadClass的逻辑,如果父类加载失败,会自动调用自己的 findClass() 方法来完成加载。这样既不影响用户按照自己的意愿去加载类,也不会破坏双亲委派规则。
- 双亲委派模型第二次破坏
双亲委派模型的第二次破坏是由这个模型自身的缺陷导致的,双亲委派机制很好的解决了各个类加载器协作时基础类型一致的问题(越基础的类由越上层的类加载器加载),基础类型之所以被称为基础,是因为它们总是作为用户代码继承、调用的API存在,但是程序设计往往没有完美的存在,如果有基础类型需要调用用户的代码,那该怎么办呢?
一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器完成,肯定属于Java中很基础的类型了。但JNDI存在的目的是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署的应用程序的 ClassPath 下的JNDI服务提供者接口的代码,启动类加载器是绝对不会认识这些类的,那该怎么办呢?
为了解决这个困境,Java设计团队引入了不太优雅的设计:线程上下文加载器。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父类中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。不过当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码来判断,为了消除这种及其不优雅的实现方式,在JDK6时,如果不是提供了java.util.ServiceLoader,以META-INF/Services中的配置信息,辅以责任链模式,才算是给SPI的加载提供了一种相对合理的解决方案。
- 双亲委派模型的第三次破坏
双亲委派模型的第三次破坏时用户追求程序动态性而导致的(如:代码热替换,模块热部署,等),就是希望Java程序能像我们电脑外设那样,接上鼠标键盘,不用重启电脑就可以使用。
OSGI 实现模块化部署的关键是它自定义的类加载机制的实现,每一个应用程序模块都有自己的一个类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGI环境下,类加载器不在是双亲委派模型推荐的树状模型,而是进一步发展为更复杂的网状结构,当收到类加载请求时,OSGI将按照以下顺序进行类搜索:
- 将以Java开头的类,委托给父类加载器
- 否则,将委派列表名单内的类,委派给父类加载器加载
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在则委派给 Fragment bundle的类加载器加载。
- 否则,查找Dynamic Import 列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
上面查找顺序中,只有前两条符合按照双亲委派模型原则,其余查找类都是在平级的类加载器中进行的。
- 双亲委派模型的破坏并不一定都是贬义的,只要有明确的目的和充分的理由,突破原有规则无疑也是一种创新。