类加载双亲委派模型

说到Java区别于其他语言的一大特性,自然很多人都会想到Java当初的愿景:一次编译,处处运行。而要实现这一目标自然离不开JVM虚拟机的功劳。为了能让编译出来的字节码可以被虚拟机正常使用,完成它的使命,其中必不可少的一个环节就是类加载过程。而类加载由于其复杂性,普通程序员可能并不一定去深入研究过,但想要拥抱更高层次的技术,熟悉类加载的原理是必然要迈过的一道坎。类加载过程分很多个环节,本文不具体阐述,只介绍其中的“双亲委派模型”这一点。

      从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器 (Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另—种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

      从Java开发人员的角度来看,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器(篇幅有限,省去具体描述)。

            启动类加载器(Bootstrap ClassLoader) 
            扩展类加载器(Extension ClassLoader) 
            应用程序类加载器(Application ClassLoader)

      我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载器之间的关系一般如下图所示:

      上图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

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

      如下方代码所示(摘录自Java SE 1.7.0_79 java.lang.ClassLoader),类加载器首先判断是否有父加载器,如果有则委托给父类加载,如果没有则使用启动类加载器进行加载,如果加载失败(报ClassNotFoundException异常),则自己才尝试进行加载:

      protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                  // First, check if the class has already been loaded
                  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 thrown if class not found
                              // from the non-null parent class loader
                        }

                        if (c == null) {
                              // If still not found, then invoke findClass in order
                              // to find the class.
                              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设计者推荐给开发者的一种类加载实现方式。所以历史上便产生了三次较大规模的“被破坏”情况。

       第一次“被破坏”:

      第一次“被破坏”其实发生在产生该模型之前,双亲委派模型是JDK 1.2才被引入的,而类加载器和java.lang.ClassLoader在JDK 1.0诞生之初就已经存在,为了兼容已经存在的自定义类加载器,Java设计者不得不作出妥协,引入了新的protected方法findClass(),如下代码所示。之前继承java.lang.ClassLoader的唯一目的就是重写loadClass方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal()这个方法的唯一逻辑就是去调用自己的loadClass()。

      protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
      }

      从前面的loadClass()可以看出,虽然该方法依然是protected修饰,但已经不提倡再去重写该类了,而是应当把自己的类加载逻辑写到findClass()方法中。

       第二次“被破坏”:

      双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?

      这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SH,Service Provider Interface)的代码,但启动类加载器不可能“认识“这些代码啊!那该怎么办?

      为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

      有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
 
       第三次“被破坏”:

      双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换(HotSwap)、模块热部署(Hot Deployment)等,说白了就是希望应用程序能像我们的计算机外设那样,接上鼠标、U盘,不用重启就能立即使用,鼠标有问题或要升级就换个鼠标,不用停机也不用重启。对于个人计算机来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是企业级软件开发者具有很大的吸引力。

      Sun公司所提出的JSR-294、JSR-277规范在与JCP组织的模块化规范之争中落败给JSR-291(即OSGi R4.2),虽然Sun不甘失去Java模块化的主导权,独立在发展Jigsaw项目,但目前OSGi已经成为了业界“事实上”的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(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)否则,类查找失败。
      上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。

      虽然双亲委派模型多次“被破坏”,但这里的破坏并没有贬义色彩。只要有足够的理由,在合理运用的基础上进行突破未尝不是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但在Java程序员中基本有一个共识:OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值