类加载及执行子系统的案例与实战

  在 Class 文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多,Class 文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是 字节码生成类加载器 这两部分的功能

对于类加载器的实际改造的两个例子:

  • 传统的类加载器:tomcat的类加载器
  • 灵活的类加载器:OSGi

对于字节码生成的过程干预的两个例子:

  • 通过动态代理技术去控制我们程序中一部分字节码的生成;
  • 可以直接修改字节码的工具Retrotranslator

Retrotranslator作用:将JDK1.5编译出来的.class文件转为可以在JDK1.3或者1.4上面部署的版本。它是“java逆向移植”工具中比较出色的一个。

一、Tomcat:正统的类加载器架构

1.1 完成功能

  主流的 Java Web 服务器,如 Tomcat、Jetty、WebLogic、WebSphere 或其他笔者没有列举的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的 Web 服务器,要解决如下几个问题:

  • 部署在同一个服务器上的两个web应用程序所使用的java类库可以实现相互隔离。
  • 部署在同一个服务器上的两个web应用程序使用的java类库可以实现相互共享。
  • 服务器本身要尽可能不受部署web程序的影响。
  • 支持 JSP 应用的 Web 服务器,大多数服务器应该支持HotSwap(热替换)功能。

两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。

例如,用户可能有 10 个使用 Spring 组织的应用程序部署在同一台服务器上,如果把 10 份 Spring 分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。

目前,有许多主流的 Java Web 服务器自身也是使用 Java
语言来实现的。因此,服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库相互独立。

JSP 文件最终要编译成 Java Class 才能由虚拟机执行,但 JSP
文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的 Class 文件。而且 ASP、PHP 和 JSP
这些网页应用也把修改后无须重启作为一个很大的 “优势” 来看待,因此 “主流” 的 Web 服务器都会支持 JSP 生成类的热替换,当然也有 “非主流” 的,如运行在生产模式(Producation Mode)下的 WebLogic 服务器默认就不会处理 JSP 文件的变化。

1.2 多个ClassPath

  为了支持上述4个功能而存在。这样每个ClassPath下面,可以存放不同的类库,被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相对应的自定义类加载器去加载放置在里面的 Java 类库。【这些路径一般都以 “lib” 或 “classes” 命名】

1.3 Tomcat 自定义类加载器及其结构

  在 Tomcat 目录结构中,有 3 组目录(“/common/*”、“/server/*” 和 “/shared/*”)可以存放 Java 类库,另外还可以加上 Web 应用程序自身的目录 “/WEB-INF/*”,一共 4 组,把 Java 类库放置在这些目录中的含义分别如下:

  • 放置在 /common 目录中:类库可被 Tomcat 和所有的 Web 应用程序共同使用。
  • 放置在 /server 目录中:类库可被 Tomcat 使用,对所有的 Web 应用程序都不可见。
  • 放置在 /shared 目录中:类库可被所有的 Web 应用程序共同使用,但对 Tomcat 自己不可见。
  • 放置在 /WebApp/WEB-INF 目录中:类库仅仅可以被此 Web 应用程序使用,对 Tomcat 和其他 Web
    应用程序都不可见。

  为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat 自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,其关系如图:
在这里插入图片描述
  上层前 3 个类加载器是 JDK 默认提供的类加载器,这 3 个加载器的作用前面已经介绍过了。而 CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/*/server/*/shared/*/WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器

  从上图的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoaderSharedClassLoader 使用,而 CatalinaClassLoaderSharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能

1.4 Tomcat 6.x 版本改进

  对于 Tomcat 的 6.x 版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loadershare.loader 项后才会真正建立 CatalinaClassLoaderSharedClassLoader 的实例,否则会用到这两个类加载器的地方都会用 CommonClassLoader 的实例代替,而默认的配置文件中没有设置这两个 loader 项,所以 Tomcat 6.x 顺理成章地把 /common、/server 和 /shared 三个目录默认合并到一起变成一个 /lib 目录,这个目录里的类库相当于以前 /common 目录中类库的作用。

二、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 等。

2.1 OSGi 中的模块(称为 Bundle)与普通的 Java 类库

共同点:

  • 都以 JAR 格式进行封装
  • 内部存储的都是 Java Package 和 Class

区别:

  • 一个 Bundle 可以声明它所依赖的 Java Package(通过 Import-Package 描述),也可以声明它允许导出发布的
    Java Package(通过 Export-Package 描述)。

  • 在 OSGi 里面,Bundle 之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖(至少外观上如此)

  • 类库的可见性能得到非常精确的控制,一个模块里只有被 Export 过的 Package 才可能由外界访问,其他的 Package 和 Class 将会隐藏起来。

  除了更精确的模块划分和可见性控制外,引入 OSGi 的另外一个重要理由是,基于 OSGi 的程序很可能(只是很可能,并不是一定会)可以实现模块级的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分,这对企业级程序开发来说是一个非常有诱惑力的特性。

2.2 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 来处理。

2.3 实例

  假设存在 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 里面,加载器之间的关系不再是双亲委派模型的属性结构,而是已经进一步发展成了一种更为复杂的、运行时才能确定的网状结构。这种网状的类加载器架构在带来更好的灵活性的同时,也可能会产生许多新的隐患。

如果出现了 Bundle A 依赖于 Bundle B 的 Package B,而 Bundle B 又依赖了 Bundle A 的 Package A,这两个 Bundle 进行类加载时就很容易发生死锁。

  总体来说,OSGi 描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,同时也有成熟框架对其提供实现支持。对于单个虚拟机下的应用,从开发初期就建立在 OSGi 是一个很不错的选择,这样便于约束依赖。但并非所有的应用都适合采用 OSGi 作为基础架构,OSGi 在提供强大功能的同时,也引入了额外的复杂度,带来了线程死锁和内存泄露的风险。

三、字节码生成技术与动态代理的实现

  JDK 里面的 javac 命令就是字节码生成技术的 “老祖宗”,并且 javac 也是一个由 Java 语言写成的程序,它的代码存放在 OpenJDK 的 langtools/src/share/classes/com/sun/tools/javac 目录中。
  在 Java 里面除了 javac 和字节码类库外,使用字节码生成的例子还有很多,如 Web 服务器中的 JSP 编译器编译时植入的 AOP 框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。

2.1 动态代理技术例子: Spring中的Bean技术

  相信许多 Java 开发人员都使用过动态代理,即使没有直接使用过 java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口,应该也用过 Spring 来做过 Bean 的组织管理。如果使用过 Spring,那大多数情况都会用过动态代理,因为如果 Bean 是面向接口编程,那么在 Spring 内部都是通过动态代理的方式来对 Bean 进行增强的。动态代理中所谓的 “动态”,是针对使用 Java 代码实际编写了代理类的 “静态” 代理而言的,它的优势不在于省去了编写代理类哪一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。

1. 动态代理如何影响字节码生成的?

  代理类通过调用代理类生成的方法,在程序运行时,可以产生一个描述代理类的字节码byte[]数组。从而控制了字节码的生成过程。

2. 字节码生成过程实质

根据 Class 文件的格式规范去拼装字节码

但在实际开发中,以 byte为单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码。对于用户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适。如果读者对动态代理的字节码拼装过程很感兴趣,可以在OpenJDK 的 jdk/src/share/classes/sun/misc 目录下找到sun.misc.ProxyGenerator的源码。

四、Retrotranslator:跨域JDK版本

Retrotranslator的作用:将JDK1.5编译出来的.class文件转为可以在JDK1.3或者1.4上面部署的版本。

4.1 JDK 每次升级新增的功能

大致可以分为以下 4 类:

  • 在编译器层面做的改进

如自动装箱拆箱,实际上就是编译器在程序中使用到包装对象的地方自动插入了很多
Integer.valueOf()、Float.valueOf()
之类的代码;变长参数在编译之后就自动转化成一个数组来完成参数传递;泛型的信息则在编译阶段就已经擦除掉了(但是在元数据中还保留着),相应的地方被编译器自动插入了类型转换代码。

  • 对 Java API 的代码增强

譬如 JDK 1.2 时代引入的 java.util.Collections 等一系列集合类,在 JDK 1.5 时代引入的 java.util.concurrent 并发包等。

  • 需要在字节码中进行支持的改动

如 JDK 1.7 里面新加入的语法特性:动态语言支持,就需要在虚拟机中新增一条 invokedynamic 字节码指令来实现相关的调用功能。不过字节码指令集一直处于相对比较稳定的状态,这种需要在字节码层面直接进行的改动是比较少见的。

  • 虚拟机内部的改进

如 JDK 1.5 中实现的 JSR-133 规范重新定义的 Java 内存模型(Java Memory Model,JMM)、CMS收集器之类的改动,这类改动对于程序员编写代码基本是透明的,但会对程序运行时产生影响。

  上述 4 类新功能中,Retrotranslator 只能模拟前两类,对于后面两类直接在虚拟机内部实现的改进,一般所有的逆向移植工具都是无能为力的,至少不能完整地或者再可接受的效率上完成全部模拟,否则虚拟机设计团队也没有必要舍近求远地改动处于 JDK 底层的虚拟机。

4.2 功能实质

  在可以模拟的两类功能中,第二类模拟相对更容易实现一些,如 JDK 1.5 引入的 java.util.concurrent 包,实际是由多线程大师 Doug Lea 开发的一套并发包,在 JDK 1.5 出现之前就已经存在(那时候名字叫做 dl.util.concurrent,引入 JDK 时由作者和 JDK 开发团队共同做了一些改进),所以要在旧的 JDK 中支持这部分功能,以独立类库的方式便可实现。Retrotranslator 中附带了一个名叫 “backport-util-concurrent.jar” 的类库(由另一个名为 “Backport of JSR 166” 的项目所提供)来代替 JDK 1.5 的并发包。

  至于 JDK 在编译阶段进行处理的那些改进,Retrotranslator 则是使用 ASM 框架直接对字节码进行处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值