1.概述
在Class文件格式与执行引擎这部分,用户的程序能直接影响的内容并不多,以什么格式存储、类型什么时候加载、怎样连接、虚拟机如何执行字节码指令等都是由虚拟机直接控制。用户程序控制的主要是字节码生成与类加载器这两部分功能。
2.案例分析
下面是关于类加载器和字节码的案例各两个。
2.1 Tomcat:正统的类加载器架构
主流的Java Web服务器,都实现了自己定义的类加载器(一般不止一个)。因为功能健全的Web服务器需要解决如下的问题:
部署在同一个服务器上的两个Web应用程序使用的Java类库可以实现相互隔离。两个应用程序可能会依赖同一个第三方类库。
部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。例如10各Spring应用程序部署在同一个服务器上,如果类库不能共享,类库使用时都被加载到服务器内存,虚拟机的方法区很容易出现过度膨胀。
服务器需要尽可能的保证自身的安全不受部署的Web应用程序影响。一般来说,基于安全考虑,服务器使用的类库应该与应用程序的类库互相独立。
支持JSP应用的Web服务器,大多数需要支持HotSwap功能。"主流"的Web服务器都会支持JSP生成类的热替换。
现在,笔者以Tomcat服务器为例,看一看Tomcat如何规划用户类库结构和类加载器的。
Tomcat目录结构中,有3组目录("/common/"、"/server/"、"/shared/*")可以存放Java类库,另外还可以加上Web应用程序自身的目录"/WEB-INF",一共4组。
/common目录下:类库可被Tomcat和所有的Web应用程序共同使用。
/server目录下:类库可被Tomcat使用,对所有的Web应用程序都不可见。
/shared目录:类库可被所有的Web应用程序共同使用,对Tomcat不可见。
/WEB-INF:类库仅仅可以被此Web应用使用,对Tomcat和其他Web应用程序不可见。
为此,Tomcat自定义类多个类加载器。关系如下图所示:
2.2 OSGi:灵活的类加载器架构
OSGi的每个模块(Bundle)与普通的Java类库区别不大。一般都以JAR格式进行封装,内部存储的是Java Package和Class。但一个Bundle可以声明它所依赖但Java Package(通过Import-Package描述),也可以声明它允许导出发布的Java package(通过Export-package描述)。
OSGi中,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖,而且类库的可见性由很准确的控制,一个模块只有被Export过的Package才能由外界访问;它还很可能实现模块级的热插拔功能,当程序升级或调试时,可以只停用、重新安装其中一部分。
OSGi之所以可以这样,是因为它灵活的类加载器。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如:某个Bundle声明了一个它依赖的Package,如果其他的Bundle声明发布了这个Package,那么所有对这个Package对类加载器动作都会委派给发布它的Bundle类加载器去完成。
另外,一个Bundle类加载器为其他Bundle提供服务时,会根据Export-Package列表严格控制访问范围。如果一个类存在于Bundle的类库中但没有被export,那么这个Bundle的类加载器能找到这个类,但不会提供给其他Bundle使用。
观察下面图片:
BundleA:声明发布了packageA,依赖java.*的包。
BundleB:声明依赖了packageA,packageC,同时依赖java.*的包。
BundleC:声明发布了packageC,依赖packageA。
从图中可以看出,OSGi里面,加载器之间不再是双亲委派模型的树形结构,而是更复杂的、运行时才能确定的网状结构。
2.3 字节码生成技术与动态代理的实现
使用字节码生成的例子很多,如Web服务器的JSP编译器,编译时植入的AOP框架,动态代理技术等。
动态代理所谓的"动态",是针对使用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("dancing????");
return method.invoke(originalObj,args);
}
}
public static void main(String[] args) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
}
}
结果:
dancing????
hello world!
上述代码里,特殊之处就是Proxy.newProxyInstance()方法,跟踪这个方法的源码,可以看到程序进行了验证、优化、缓存、同步、生成字节码、显式类加载等操作,而最后它调用了sun.misc.ProxyGenerator.generateProxyClass()方法完成生成字节码的动作。这个方法可以再运行时产生一个描述代理类的字节码byte[]数组。
如果想看运行时代理类写了什么,可以再main()方法中加入下面这句:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
运行程序,磁盘中会产生一个文件"$Proxy.class"的代理类Class文件,如下:
import DynamicProxyTest.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
final class $Proxy0 extends Proxy implements IHello {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void sayHello() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("DynamicProxyTest$IHello").getMethod("sayHello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
大致的生成过程是根据Class文件的格式规范去拼装字节码。如果对字节码拼装过程感兴趣,可以再OpenJDK的/jdk/src/share/classes/sun/misc目录下找到sun.misc.ProxyGenerator的源码。
2.4 Retrotranslator:跨越JDK版本
Java中,每次JDK大版本的发布,都伴随着一场大规模的技术革新,有时候会因为要保护现有投资、维持程序结构稳定,需要使用以前的版本的JDK。于是一种"Java逆向移植"的工具应运而生,Retrotranslator是最典型的一个。
举例来说,它可以将JDK1.5版本编译出来的Class文件转变为1.4版本上部署的版本,可以很好的支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入等,还可以支持1.5中新增的集合改进、并发包以及对泛型、注解等反射操作。
想知道它如何做到,首先要弄清楚JDK升级中会提供哪些功能。JDK每次升级大致分为4类:
编译器层面做的改进。如自动装箱拆箱,实际上就是编译器在程序中使用包装对象的地方自动插入了Integer.valueOf();变长参数在编译后自动转化成一个数组完成参数传递;
对Java API的代码增强。引入新的包。
在字节码中进行支持的改动。如JDK1.7里新的语法特性:动态语言支持。
虚拟机内部改进。
上述4类,Retrotranslator只能模拟前2类,例如JDK1.5引入的java.util.concurrent包,要在旧的JDK中支持这部分功能,以独立类库的方法可以实现。Retrotranslator中附带类"backport-util-concurrent.jar"的类库。
至于JDK在编译阶段进行处理的改进,Retrotranslator使用ASM框架直接对字节码进行处理。例如枚举,Retrotranslator对枚举所做的主要处理是把枚举类的父类从"java.lang.Enum"替换为它运行时类库中包含的"net.sf.retrotranslator.runtime.java.lang.Enum",然后在类和字段的访问标志中抹去ACC_ENUM标志位。