OSGi:灵活的类加载器架构。

Java程序社区中流传着这么一个观点:“学习JEE规范,去看JBoss源码;学习类加载器,就去看OSGi源码”。机关“JEE规范”和“类加载器的知识”并不是一个对等的概念,不过,既然这个观点能在程序员中流传开来,也从侧面说明了OSGi对类加载器的运用确实有其独到之处。
OSGi(Open Service Gateway Initiative)是OSGi联盟(OSGi Alliance)制定的一个基于Java语言的动态模块化规范,这个规范最初由Sun、IBM、爱立信等公司联合发起,目的是使服务提供商通过住宅网管为各种家用智能是设备提供各种服务,后来这个规范在Java的其他技术领域也有相当不错的发展,现在已经成为Java世界中“事实上”的模块化标准,并且已经有了Equinox、Felix等成熟的实现。OSGi在Java程序员中最著名的应用案例就是Eclipse IDE,另外还有许多大型的软件平添和中间件服务器都基于或声明将会基于OSGi规范来实现,如IBM Jazz平台、GlassFish服务器、jBoss OSGi等。
OSGi中的每个模块(称为Bundle)与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明他所依赖的Java Package(通过Import-Package描述),也可以声明他允许导出发布的java Package(通过Export-Package描述)。在OSGi里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此),而且类库的可见性能得到非常精确的控制,一个模块里只有被Export过的package才可能由外界访问,其他的package和class将会被隐藏起来。除了更精确的模块划分和可见性控制外,引入OSGi的另外一个重要理由是,基于OSGi的程序很可能(只是很可能,并不是一定会)可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑力的特性。
OSGi之所以能有上述“诱人”的特点,要归功于他灵活的类加载器架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundle声明了一个他依赖的Package,如果有其他Bundle声明发布了这个Package,那么所有对这个Package的类加载动作都会委派给发布他的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器都是平级关系,只有具体使用某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖。
另外,一个Bundle类加载器为其他的Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类库中但没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用,而且OSGi平台也不会把其他Bundle的类加载请求分配给这个Bundle来处理。
我们可以举一个更具体一些的简单例子,假设存在Bundle A、Bundle B、Bundle C三个模块,并且这三个Bundle定义的依赖关系如下。

  • Bundle A:声明发布了packageA,依赖了java.*的包。
  • Bundle B:声明依赖了packageA和packageC,同时也依赖了java.*的包。
  • Bundle C:声明发布了packageC,依赖了packageA。

那么,这三个Bundle之间的类加载器及父类加载器之间的关系如下图所示。

由于没有牵扯到具体的OSGi实现,所以上图中的类加载器都没有指明具体的加载器实现,只是一个体现了加载器之关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi中,加载一个类可能发生的查找行为和委派关系会比上图显示的复杂得多,类加载时可能进行的查找规则如下:

  • 以java.*开头的类,委派给服了加载器加载。
  • 否则,委派列表名单内的类,委派给父类加载器加载。
  • 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  • 否则,查找当前Bundle的Classpath,使用自己的了加载器加载。
  • 否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment Bundle的类加载器加载。
  • 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  • 否则,类查找失败。

从上图中还可以看出,在OSGi里面,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成了一种更为复杂的、运行时才能确定的网状结构。这种网状的类加载器架构在带来更好的灵活性的同时,也可能会产生许多新的隐患。曾经参与过将一个非OSGi的大型系统向Equinox OSGi平台迁移的项目,由于历史原因,代码模块之间的依赖关系错综复杂,勉强分离出各个模块的Bundle后,发现在高并发环境下经常出现死锁。我们很容易就找到了死锁的原因:如果出现了Bundle A依赖Bundle B的Package B,而Bundle B又依赖了Bundle A的Package A,这两个Bundle进行类加载时就很容易发生死锁。具体情况是当Bundle A加载Package B的类时,首先需要锁定当前类加载器的实例对象(java.lang.ClassLoader.loadClass()是一个synchronized方法),然后把请求委派给Bundle B的加载器处理,但如果这时候Bundle B也正好想加载Package A的类,他也先锁定自己的加载器再去请求Bundle A的加载器处理,这样,两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,因此他们就互相死锁,永远无法完成加载请求了。Equinox的Bug List中有关于这类问题的Bug,也提供了一个以牺牲性能为代价的解决方案——用户可以启用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动作。在JDK 1.7中,为非树状继承关系下的类加载器架构进行了一次专门的升级,目的是从底层避免这类死锁出现的可能。
总体来说,OSGi描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,同时也有成熟框架对其提供实现支持。对于单个虚拟机下的应用,从开发初期就建立在OSGi上是一个很不错的选择,这样便于约束依赖。但并非所有的应用都适合采用OSGi作为基础架构,OSGi在提供强大功能的同时,也引入了额外的复杂度,带来了线程死锁和内存泄漏的风险。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值