目录
类加载器简介
双亲委派模型
破坏双亲委派模型
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进 制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取 所需要的类。实现这个动作的代码模块被称为"类加载器'。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远 限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。这句话可以表达得更通俗一些:
比较两个类是否”相 等“,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两 个类是来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。
这里所指的”相等“,包括代表类的Class对象的equals()方法、isAssignableFrom() 方法、islnstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判 定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。
构造了一个简单的类加载器,尽管很简陋,但是对于这个演示 说还是够用了。它可以加载与自己在同一路径下的Class文件。我们使用这个类加载 器去加载了一个名为"org. fenixsoft.classloading. ClassLoaderTest"的类,并实例化了 这个类的对象。
两行输出结果中,从第一句可以看到这个对象确实是类org. f enbcsoft. classloading.ClassLoaderTest实例化出来的对象,但从第二句可以发现这个对象与类org. fenixsoft.classloading.ClassLoaderTest做所属类型检查的时候却返回了false, 这是因为 虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个 独立的类,做对象所属类型检查时结果自然为false。
站在Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器 (Bootstrap ClassLoader), 这个类加载器使用C++语言实现,是虚拟机自身的一部分; 另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外 部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,类加载器就还可以划分得更细致一些,绝大部分Java 程序都会使用到以下三种系统提供的类加载器:
启动类加载器(Bootstrap ClassLoader) : 这个类加载器负责将 存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclass path参数所指定的路径 中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar, 名字不符合的类库即 使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法 披Java程序直接引用。
扩展类加载器(Extension Class Loader) : 这个加载器由sun. misc.Launcher.ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被 java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader) : 这个类加载器由sun.misc. Launcher$AppC lassLoader来实现。由于这个类加载器是Class Loader中的 getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责 加载用户类路径(ClassPatb)上所指定的类库,开发者可以直接使用这个类加载 器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中 默认的类加载器。
我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以 加入自己定义的类加载器。这些类加载器之间的关系一般会如图
所展示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余 的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继 承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载 器的代码。
类加载器的双亲委派模型在JDK 1.2期间被引入并被广泛应用于之后儿乎所有的 Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的 一种类加载器实现方。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会 自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加 载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当 父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类 随若它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object, 它 放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器 进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有 使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java. tang.Object的类,并放在程序的Class Path中,那系统中将会出现多个不同的Object类, Java类型体系中最基础的行为也就无从保证,应用程序也将会变得一片混乱。如果您有 兴趣的话,可以尝试去写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常 编译,但永远无法被加载运行。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,如 现双亲委派的代码都集中在java.lang.ClassLoader的load Class()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的 load Class()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,则在抛出ClassNotFoundException异常后,再调用 加载自己的findClass()方法进行加载。
文提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者们推荐给 开发者们的类加载器实现方式。在Java的世界里面大部分的类加载器都遵循这个模型, 但也有例外的情况,到现在为止,双亲委派模型主要出现过三次较大规模的“被破坏” 情况。
双亲委派模型的第一次“被破坏“其实发生在双亲委派模型出现之前,jdk1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入的,而类加载器和抽象类 java .lang. ClassLoader则在JDK 1.0时代就已经存在,面对已经存在的用户自定义类加载 器的实现代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了向前兼容, JDK 1.2之后的java. lang. ClassLoader添加了一个新的protected方法find Class(), 在此 之前,用户去继承java.lang. ClassLoader的唯一目的就是为了重写loadClass()方法,因 为虚拟机在进行类加载的时候会调用加载器的私有方法loadClasslnternal(), 而这个方法 的唯一逻辑就是去调用自己的load Class()。
我们已经看过load Class()方法的代码,双亲委派的具体逻辑就实现在这个方 法之中,JDK 1.2之后已不提倡用户再去覆盖load Class()方法,而应当把自己的类加载 逻辑写到find Class()方法中,在load Class()方法的逻辑里如果父类加载失败,则会调用 的find Class()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型规则的。
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委 派很好地解决了各个类加载器的基础类的统一间题(越基础的类由越上层的加载器 进行加载),基础类之所以被称为“基础",是因为它们总是作为被用户代码调用的 API, 但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么 办了?
这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的 标准服务,它的代码由启动类加载器去加载(在JDK 1.3时代放进去的rt.jar), 但JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序 的Class Path下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动 加载器不可能“认识”这些代码啊!那该怎么办?
为了解决这个困堍,Java设计团队只好引入了一个不太优雅的设计:线程上下类加载器(Thread Context Class Loader)。这个类加载器可以通过java.lang.Thread类的 setCon textClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继 承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用 程序类加载器。
有了线程上下文类加载器,就可以做一些“舞弊"的事情了,JNDI服务使用这个 线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去 元成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用 类加载器,已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java 中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB 和JBI等。
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这 所说的“动态性”指的是当前一些非常“热”门的名词:代码热替换(HotSwap)、模块 热部署(Hot Deployment)等,说白了就是希望应用程序能像我们的电脑外设那样,插 上鼠标或U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用 停机也不用重启。对于个人电脑来说,重启一次其实没有什么大不了的,但对于一些生 产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发 很重要,尤其是企业级软件开发者具有很大的吸引力。
在JSR-297 <D、JSR-277。规范从纸上标准变成真正可运行的程序之前,OSGi是、 前业界”事实上”的Java模块化标准,而OSGi实现模块化热部署的关键则是它自定 义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的 类加载器,当要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代 码的热替换。
在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为 网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:
(I)将以java. • 开头的类,委派给父类加载器加载。
(2)否则,将委派列表名单内的类,委派给父类加载器加载。
(3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器 加载。
(4)否则,查找当前Bundle的ClassPath, 使用自己的类加载器加载。
(5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
(6)否则,查找Dynamic Import列表的Bundle, 委派给对应Bundle的类加载器 加载。
(7)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级 的类加载器中进行的。
虽然使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为,被破坏并不带有贬义的感情色彩。只要有足够意义和理由,突破已有的原则就 可算作一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,并且 业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但在Java程序员中 基本有一个共识: OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,自 然就明白了类加载器的精粹。
