Java核心技术之类加载及执行子系统

 在Class文件格式与执行引擎这部分中,用户能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能。

Tomcat:正统的类加载器架构

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

  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。 这是最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。
  • 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。 例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区就会很容易出现过度膨胀的风险。
  • 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。 目前,有许多主流的Java Web服务器自身也是使用Java语言来实现的。因此,服务器本身也有类库依赖的问题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。
  • 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。 我们知道,JSP文件最终要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web服务器都会支持JSP生成类的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务器默认就不会处理JSP文件的变化。

Tomcat加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。

OSGi:灵活的类加载器架构

 OSGi(Open Service Gateway Initiative)是OSGi联盟(OSGi Alliance)制定的一个基于Java语言的动态模块化规范。

 OSGi中的每个模块(称为Bundle)与普通的Java类库区别并不太大,两者一般都以JAR格式进行封装,并且内部存储的都是Java Package和Class。但是一个Bundle可以声明它所依赖的Java Package(通过Import-Package描述),也可以声明它允许导出发布的JavaPackage(通过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之间的类加载器及父类加载器之间的关系
在这里插入图片描述
类加载时可能进行的查找规则如下:

以java.*开头的类,委派给父类加载器加载。
否则,委派列表名单内的类,委派给父类加载器加载。
否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
否则,查找是否在自己的Fragment Bundle中,如果是,则委派给Fragment Bundle的类加载器加载。
否则,查找Dynamic Import列表的Bundle,委派给对应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的加载器处理,这样,两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,因此它们就互相死锁,永远无法完成加载请求了。

在JDK 1.7中,为非树状继承关系下的类加载器架构进行了一次专门的升级,目的是从底层避免这类死锁出现的可能。

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

 在Java里面除了javac和字节码类库外,使用字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时植入的AOP框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。

动态代理的简单示例

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();
	}
}

运行结果如下:

welcome
hello world

 上述代码里,唯一的“黑匣子”就是Proxy.newProxyInstance()方法,除此之外再没有任何特殊之处。这个方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的对象。跟踪这个方法的源码,可以看到程序进行了验证、优化、缓存、同步、生成字节码、显式类加载等操作,前面的步骤并不是我们关注的重点,而最后它调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作,这个方法可以在运行时产生一个描述代理类的字节码byte[]数组。如果想看一看这个在运行时产生的代理类中写了些什么,可以在main()方法中加入下面这句:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles""true");

加入这句代码后再次运行程序,磁盘中将会产生一个名为“$Proxy0.class”的代理类Class文件,反编译
反编译的动态代理类的代码

package org.fenixsoft.bytecode;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy
implements DynamicProxyTest.IHello
{
private static Method m3;
private static Method m1;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
public final void sayHello()
throws
{
try
{
this.h.invoke(this,m3,null);
return}
catch(RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch(Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
//此处由于版面原因,省略equals()、hashCode()、toString()三个方法的代码
//这3个方法的内容与sayHello()非常相似。
static
{
try
{
m3=Class.forName("org.fenixsoft.bytecode.DynamicProxyTest $IHello".getMethod("sayHello"new Class[0]);
m1=Class.forName("java.lang.Object".getMethod("equals"new Class[]{Class.forName("java.lang.Object"});
m0=Class.forName("java.lang.Object".getMethod("hashCode"new Class[0]);
m2=Class.forName("java.lang.Object".getMethod("toString"new Class[0]);
return}
catch(NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch(ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}

 这个代理类的实现代码也很简单,它为传入接口中的每一个方法,以及从java.lang.Object中继承来的equals()、hashCode()、toString()方法都生成了对应的实现,并且统一调用了InvocationHandler对象的invoke()方法(代码中的“this.h”就是父类Proxy中保存的InvocationHandler实例变量)来实现这些方法的内容,各个方法的区别不过是传入的参数和Method对象有所不同而已,所以无论调用动态代理的哪一个方法,实际上都是在执行InvocationHandler.invoke()中的代理逻辑。
 这个例子中并没有讲到generateProxyClass()方法具体是如何产生代理类“$Proxy0.class”的字节码的,大致的生成过程其实就是根据Class文件的格式规范去拼装字节码,但在实际开发中,以byte为单位直接拼装出字节码的应用场合很少见,这种生成方式也只能产生一些高度模板化的代码。对于用户的程序代码来说,如果有要大量操作字节码的需求,还是使用封装好的字节码类库比较合适。如果读者对动态代理的字节码拼装过程很感兴趣,可以在OpenJDK的jdk/src/share/classes/sun/misc目录下找到sun.misc.ProxyGenerator的源码。

Retrotranslator:跨越JDK版本

 Retrotranslator的作用是将JDK 1.5编译出来的Class文件转变为可以在JDK 1.4或1.3上部署的版本,它可以很好地支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入这些语法特性,甚至还可以支持JDK 1.5中新增的集合改进、并发包以及对泛型、注解等的反射操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值