第九章 类加载及执行于系统的案例与实战*
9.1 概述
在Class 文件格式与执行引擎这部分中,用户的程序能直接影响的内容并不太多, Class文件以何种格式存储,类型何时加载、如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能。
9.2 案例分析
-
Tomcat :正统的类刀。载器架构
功能健全的Web服务器需要解决的问题:
- 部署在同一个服务器上的两个Web 应用程序所使用的Java 类库可以实现相互隔离。
- 部署在同一个服务器上的两个Web 应用程序所使用的Java 类库可以互相共享。
- 服务器需要尽可能地保证自身的安全不受部署的We b 应用程序影响。
- 支持JSP 应用的Web 服务器,大多数都需要支持HotSwap 功能。(修改后无须重启,热交换)
Tomcat 提供了好几个ClassPath 路径供用户存放第三方类库,这些路径一般都以“ lib ”或“ classes ”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java 类库。
Tomcat 目录:
- 放置在/common 目录中:类库可被Tomcat 和所有的Web 应用程序共同使用。
- 放置在/server 目录中:类库可被Tomcat 使用,对所有的Web 应用程序都不可见。
- 放置在/shared 目录中:类库可被所有的Web 应用程序共同使用,但对Tomcat 自己不可见。
- 放置在/WebApp/WEB-INF 目录中:类库仅仅可以被此Web 应用程序使用,对Tomcat 和其他Web 应用程序都不可见。
为了支持这套目录结构,并对目录里面的类库进行加载和隔离, Tomcat 自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,结构图如下:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是Tomcat 自己定义的类加载器,它们分别加载
/common/*
、/server/*
、/shared/*
和/WebApp/WEBINF/*
中的Java 类库。其中 WebApp 类加载器和Jsp 类加载器通常会存在多个实例,每一个Web 应用程序对应一个WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。 Tomcat 6.x 把 /common 、/server 和 /hared三个目录默认合并到一起变成一个 /lib 目录, 这个目录里的类库相当于以前 /common 目录中类库的作用。用户可以通过修改配置文件指定 server.loader 和share.loader 的方式重新启用Tomcat 5.x 的加载器架构。
-
OSGi :灵活的类加载器架构
OSGi (Open Service Gateway Initiative )是OSGi 联盟(OSGi Alliance )制定的一个基于Java 语言的动态模块化规范,OSGi 在Java 程序员中最著名的应用案例就是 Eclipse IDE ,另外还有许多大型的软件平台和中间件服务器都基于或声明将会基于OSGi 规范来实现,如 IBM Jazz 平台、GlassFish 服务器、jBossOSGi 等。
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 的 Bundle 类加载器之间只有规则,没有固定的委派关系。
一个Bundle 类加载器为其他Bundle 提供服务时,会根据Export-Package 列表严格控制访问范围。如果一个类存在于Bundle 的类库中但是没有被Export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle 使用,而且OSGi 平台也不会把其他Bundle 的类加载请求分配给这个Bundle 来处理。
假设存在B undle A 、Bundle B 、Bundle C 三个模块,并且这三个Bundle 定义的依赖关系如下。
- Bundle A:声明发布了packageA ,依赖了 java.*的包。
- Bundle B:声明依赖了 packageA 和 packageC ,同时也依赖了 java.* 的包。
- Bundle C:声明发布了 packageC ,依赖了 packageA 。
这三个B undle 之间的类加载器及父类加载器之间的委派关系(概念模型)如下:
类加载时可能进行的查找规则如下:
- 以 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 在提供强大功能的同时,也引入了额外的复杂度,带来了线程死锁和内存泄漏的风险。
-
字节码生成技术与动态代理的实现
Spring 内部都是通过动态代理的方式来对Bean 进行增强的,动态代理中所谓的“动态”, 是针对使用Java 代码实际编写了代理类的“静态” 代理而言的, 它的优势不在于省去了编写代理类那一点工作量,而是实现了可以在原始类和接口还未知的时候, 就确定代理类的代理行为, 当代理类与原始类脱离直接联系后,就可以很灵活地重用于不同的应用场景之中。
public class DynamicProxyTest { interface IHello { void sayHello(); } static class Hello implements IHello { @Override public void sayHello() { System.out.println("hello world"); } } static class DynamicProxy implements InvocationHandler { Object originalObj; Object bind(Object originalObj) { this.originalObj = originalObj; return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("welcome"); return method.invoke(originalObj, args); } } public static void main(String[] args) { IHello hello = (IHello) new DynamicProxy().bind(new Hello()); hello.sayHello(); } }
-
Retrotranslator:跨越JDK 版本
在Java 世界里,每一次JDK 大版本的发布,都伴随着一场大规模的技术革新,而对Java 程序编写习惯改变最大的,无疑是JDK 1.5 的发布。但如何跨越Java 的版本去旧的JDK中运行新程序呢?“ Java 逆向移植”的工具( Java Backporting Tools )应运而生, Retrotranslator 8 是这类工具中较出色的一个。Retro trans l ator 的作用。
Retro trans l ator 的作用是将JDK 1.5 编译出来的Class 文件转变为可以在JDK 1.4 或1.3上部署的版本,它可以很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导人这些语法特性, 甚至还可以支持JDK 1.5 中新增的集合改进、并发包以及对泛型、注解等的反射操作。
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 内存模型Ciava Memory Model, JMM )、CMS 收集器之类的改动,这类改动对于程序员编写代码基本是透明的,但会对程序运行时产生影响。
上述4 类新功能中, Retrotranslator 只能模拟前两类,在可以模拟的两类功能中,第二类模拟相对更容易实现一些,而编译阶段进行处理的那些改进, Retrotranslator 则是使用ASM 框架直接对字节码进行处理。由于组成Class 文件的字节码指令数量并没有改变,所以无论是JDK 1.3 、JDK 1.4 还是JDK M ,能用字节码表达的语义范围应该是一致的。
9.3 实战:自己动手实现远程执行功能
P290
9.4 小结
第 9 章介绍了Class 文件格式、类加载及虚拟机执行引擎几部分内容,这些内容是虚拟机中必不可少的组成部分,只有了解了虚拟机如何执行程序, 才能更好地理解怎样写出优秀的代码。
评论区