类加载及执行子系统的案例分析

概述

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

案例分析

Tomcat:正统的类加载架构

主流的Java Web服务器如Tomcat、Jetty、WebLogic、WebSphere都实现了自己的类加载器。一个功能健全的服务器,要解决如下几个问题:

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

基于上述问题,在部署web应用时,单独的一个ClassPath就无法满足需求了,所以各种服务器都提供了好几个ClassPath路径供用户存放第三方类库,这些路径一般以lib或classes命名。放置到不同路径中的类库,具备不同的访问范围和服务对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的java类库。

Tomcat5之前,类库存放在四组目录中:”/commom”、”/server”、”/shared”和Web应用自身目录”/WEB-INF”,现在,前三个目录都被合并到lib目录下,不过可以在conf/catalina.properties查看三者指定的加载器,其中common.loader指定了加载器路径为lib,其余两项为空,除非自己修改server.loader和shared.loader,否则默认使用CommonClassLoader代替。下面是四个目录的区别:

  • common:类库可被Tomcat和所有的Web应用程序共同使用;
  • server:类库可被Tomcat使用,对所有Web应用程序都不可见;
  • shared:类库可被所有Web应用程序共同使用,但对Tomcat自己不可见;
  • WEB-INF:类库仅仅可以被次Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

Tomcat定义了多个类加载器,按双亲委派模型实现,如图所示:
这里写图片描述
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader是Tomcat自定义的加载器,分别对应common、server、shared和WEB-INF的类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

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

要在Tomcat部署多个Spring应用程序,可以把Spring放到lib目录下让这些程序共享,Spring可以通过线程上下文类加载器由父类加载器访问子类加载器,创建线程时会把类加载器设置为WebApp类加载器。

OSGi(Open Service Gateway Initiative):灵活的类加载器架构

OSGi是OSGi联盟制定的一个基于Java语言的动态模块化规范,目的是使服务提供商通过住宅网关为各种家用智能设备提供各种服务,现在已经成为了Java世界中事实上的模块化标准。著名的案例是Eclipse IDE,另外还有许多大型的软件平台和中间件服务器都基于或声明将会基于OSGi规范来实现。

OSGi中的每个模块(Bundle)与普通的Java类库区别不大,两者一般都以JAR格式进行封装,并且内部存储的是Java Package和Class,但是一个Bundle可以声明他所依赖的Java Package(Import-Package),也可以声明它允许导出发布的Java Package(Export-Package)。在OSGi里,Bundle之间的依赖关系从传统的上层模块依赖底层模块变为平级模块之间的依赖,而且类库可见性得到精确的控制。此外,基于OSGi的程序很可能可以实现模块级别的热插拔功能,当程序升级更新或调试除错时,可以只停用、重新安装然后启用程序的其中一部分。

OSGi之所以有上述特点,要归功于它灵活的类加载器架构,OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。不涉及具体的package时,各个Bundle加载器都是平级关系,只有具体使用某个package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派和依赖。加载一个类可能发生的查找行为和委派关系规则如下:

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

由于loadClass是synchronized方法,在加载时容易造成死锁,可以用osgi.Classloader.singleThreadLoads参数强制按单线程方式加载,JDK1.7也为非树状继承关系下的类加载器架构进行了一次专门的升级,从底层避免死锁。

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

使用字节码生成技术的例子有很多,如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

在上面的代码中,newProxyInstance方法调用了generateProxyClass方法来完成生成字节码的动作,这个方法可以在运行时产生一个描述代理类的字节码数组。

Retrotranslator:跨越JDK版本

要把新版本的代码放到旧版本JDK环境中去部署使用,需要使用Java逆向移植的工具(Java Backporting Tools),如Retrotranslator。

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

JDK每次新增的功能大致分为以下四类:

  • 在编译器层面做的改进。如自动装箱拆箱,实际上就是编译器在程序中使用到包装对象的地方自动插入了很多Integer.valueOf()之类的代码,变长参数在编译之后就变成了一个数组来完成传递,泛型的信息则在编译阶段就已经擦除掉了,相应的地方被编译器自动插入了类型转换代码;
  • 对Java API的代码增强。譬如1.2引入的Collections、1.5引入的concurrent并发包等;
  • 需要在字节码中进行支持的改动。如1.7加入的动态语言支持,为字节码增加一条invokedynamic指令;
  • 虚拟机内部的改进。如1.5实现的JSR-133规范重新定义的Java内存模型、CMS收集器之类的改动。

上述四类新功能中,Retrotranslator只能模拟前两类,第二类模拟更容易实现一些。以concurrent为例,Retrotranslator附带了一个名叫“backport-util-concurrent.jar”的类库来代替。

Retrotranslator使用ASM框架直接对字节码进行处理,来处理JDK在编译层面做的改进。由于组成Class文件的指令不变,所以能用字节码表示的语义范围不变,但是元数据信息和一些语法支持的内容还是要做相应的修改。例如枚举类,Retrotranslator所做的处理就是把枚举类的父类从“java.lang.Enum”替换为它运行时类库中包含的“net.sf.retrotranslator.runtime.java.lang.Enum_”,然后在类和字段的访问标志中抹去ACC_ENUM标志位。同时,value()和valueOf()方法都要重写,常量池需要引入大量的新的来自父类的符号引用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值