深入理解JVM(③)虚拟机的类加载器(双亲委派模型) (2)

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶端的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系的好处就是能保证Java类型体系中最基础的类的唯一。
例如:类java.lang.Object无论哪一个类加载器加载最终都会委派给启动类加载器,因此能够保证各种类加载器环境中的都是同一个类。这样就能保证我们创建出来的类的拥有最基础的行为。

记得以前在面试的时候有被问到,让自己写一个java.lang.String类然后会被虚拟机加载运行吗?这个问题考察的就是类的加载机制的双亲委派模型。

双亲委派模型的源码其实非常简洁,先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadeClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父加载器加载失败,抛出ClassNotFoundException异常,才会调用自己的findClass方法尝试进行加载。
源码如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查请求的类是否已经被加载过了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出ClassNotFoundException // 说明父类加载器无法完成加载请求 } if (c == null) { // 在父类加载器无法加载时 // 在调用本身的findClass方法来进行类加载 long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } 破坏双亲委派模型

上面也提到了,双亲委派模型并不是一个具体强制性约束的模型,虽然在Java的世界大部分的类加载器都遵循这个模型,但也有例外的情况,直到JDK9(模块化)为止,主要出现过3次较大规模“被破坏”的情况。

第一次“被破坏”其实发生在双亲委派模型出现之前,由于双亲委派模型在JDK1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在了。为了向前兼容,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写类的加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

第二次“被破坏”是因为自身的一些局限性导致的,双亲委派模型很好的解决了各个类加载器协作时基础类型的一致性问题,即越基础的类由越上层的加载器进行加载。
但是如果基础的类需要调用下面的用户代码时该怎么办呢?Java设计团队用了一个不太优雅的方案,引入了一个名叫线程上下文的类加载器(Thread Context ClassLodar)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
像JNDI、JDBC、JCE、JAXB、JBI等都是用的这种类型加载器实现的功能。最常见的tomcat中就用到了线程上下文的类加载器。

第三次“破坏”,是为了实现热部署、模块化。在更新了一部分代码后,不需要停机重启,只需要将类加载器和类都替换掉就可以了。典型的就是OSGi的模块化热部署。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygxsj.html