代码编译的结果从本地机器码变成了字节码是储存格式发展的一小步,却是编程语言发展的一大步
class文件以何种格式存储,类型何时加载,如何连接,以及虚拟机如何执行字节码指令等都是由虚拟机直接控制的行为,用户程序无法对其进行改变
能通过程序进行操作的,主要是字节码生成与类加载器这两部分的功能
Tomcat:正统的类加载器架构
主流的web服务器都实现了自己定义的类加载器(一般不止一个)
一个功能健全的web服务器需要解决以下问题:
1)部署在同一个服务器上的两个web应用程序所使用的java类库可以实现互相隔离(同一类库的不同版本)
2)部署在同一个服务器上的两个web应用程序所使用的java类库可以实现互相共享(同一类库的相同版本例如:Spring)
造成的不是磁盘空间的占用,而是类库使用时候需要加载到内存,容易出现方法区膨胀
3)服务器需要尽可能的保证自身的安全不受部署的web应用程序影响.目前有很多服务器本身也是java写的,本身也存在类库的依赖问题,基于安全考虑,一般来说服务器所用类库需要与应用程序类库独立
4)支持JSP的服务器,大多数需要支持HotSwap功能
由于存在上诉问题,单独的一个ClassPath就无法满足需求了,因此各种web服务器都提供了好几个ClassPath路径供用户存放第三方类库
在Tomcat6.0之前,其目录结构中有三组目录("/common/* ", "/server/* ", “/shared/* “)可以存放java类库,加上web应用自己的”/WEB-INF/*”, 一共有4组
- /common:可以被Tomcat和所有的web程序使用
- /server:只能被Tomcat使用
- /shared:对所有web应用可用,Tomcat本身不可用
- /WEB-INF:只对当前应用可用
为了支持这套目录结构,Tomcat定义了多个类加载器.
从图中关系可以看出,Tomcat自己实现的类加载器之间有关联,也有互相隔离的部分,其中webApp加载器和JSP加载器通常存在多个实例(一个应用对应一个WebApp类加载器,一个JSP对应一个Jsp类加载器)
以JSP类加载器来说,它的加载范围仅仅是这个JSP文件所编译出来的那个Class,它的出现就是为了被丢弃,当JSP文件被修改时,会替换掉当前的JasperLoader实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap
Tomcat6.0之后,只有指定了catalina,properties配置文件的server.loader和share.loader项之后才会真正建立CatalinaClassLoader和SharedClassLoader实例,否则都默认使用CommonClassLoader实例,默认是没有这两个loader项的,因此目录结构也只有/lib一个,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用Tomcat5.0加载器架构
OSGI:灵活的类加载器架构
OSGi(Open Service Gateway Initiative)技术是Java动态化模块化系统的一系列规范。OSGi一方面指维护OSGi规范的OSGI官方联盟,另一方面指的是该组织维护的基于Java语言的服务(业务)规范。简单来说,OSGi可以认为是Java平台的模块层。
OSGi服务平台向Java提供服务,这些服务使Java成为软件集成和软件开发的首选环境。Java提供在多个平台支持产品的可移植性。OSGi技术提供允许应用程序使用精炼、可重用和可协作的组件构建的标准化原语,这些组件能够组装进一个应用和部署中。
OSGI特点:
- OSGI中的每个模块(Bundle)与普通的Java类库区别并不太大,两者都是以jar格式进行封装,并且内部存储的都是Java Package和Class.
- 一个Bundle可以声明它所依赖的Java Package(Import-Package).也可以声明它允许导出发布的Java Package(Export-Package).在OSGI里面,Bundle之间的依赖关系从传统的上层模块依赖底层模块变成平级模块之间的依赖,类库的可见性得到了非常精准的控制(只有被Export的Package才可能由外界访问)
- 基于OSGI的程序可能可以实现模块级的热插拔
OSGI的特点归功于其灵活的类加载器架构.OSGI的Bundle类加载器之间只有规则,没有固定的委派关系.
某个Bundle声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package,那么所有对该Package的类加载动作都会委派给发布它的Bundle类加载器去完成,当不涉及具体的Package的时候,所有的类加载器之间都是平级关系,只有具体使用的时候,才会根据Package导入导出来定义Bundle之间的委派和依赖
例如:
Bundle A: 发布了Package A 依赖了java.*包
Bundle B:依赖了Package A和Package C, java.*包
Bundle C:发布了Package C, 依赖了Package A
那么三个Bundle之间的类加载器及其父类加载器之间的关系如上图,可以看出,加载器之间的关系已经不是双亲委派模型的树形结构,而是发展成了一种运行时才能确定的网状结构
在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进行类加载的时候就很容易发生死锁
原因:
当Bundle A加载Package B时,首先需要锁定当前类加载器的实例对象,而loadClass方法是一个synchronized方法,然后把请求委派给Bundle B的加载器处理,如果这个时候Bundle B也正好想加载Package A的类,它也锁定自己的加载器再去请求Bundle A的加载器处理,这样,两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,它们就死锁了
解决方案:
启用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动作
在JDK1.7之后,为非树状继承关系下的类加载器架构进行了一次专门的升级,就是为了解决这类问题
总结
OSGI描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,但是才用OSGI引入了额外的复杂度,带来了线程死锁和内存泄漏的风险
字节码生成技术与动态代理的实现
JDK中的javac命令就是字节码生成技术的"老祖宗"
动态代理相比于静态代理,优势不在于省去了编写代理类的工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活的重用与不同场景中
在动态代理中,最重要的方法就是
Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces, InvocationHandler h)
该方法返回一个实现了指定接口并且代理了指定方法的代理对象,跟踪这个方法的源码,我们看到程序进行了验证,优化,缓存,同步,生成字节码,显示类加载等操作,最后他调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成字节码生成的操作,
字节码的大致生成过程就是根据Class文件的格式规范去拼装字节码,该方式只能生产一些高度模板化的代码,如果有大量字节码操作应该使用封装好的字节码类库
Retrotranslator: 跨越JDK版本
Restrotranslator的作用是将JDK1.5编译出来的Class文件转变为可以在JDK1.4或1.3上部署的版本,他可以很好的支持自动装箱,泛型,动态注解,枚举,静态导入等语法特性,甚至还可以支持1.5中新增的集合改进,并发包等
JDK的更新可以大致分为4类
- 编译层面的改进,例如自动装箱,泛型,可变参数
- API的加强,例如1.5引入的concurrent并发包
- 字节码相关改动,例如1.7的动态语言支持
- 虚拟机内部改进
Restrotranslator可以模拟前两类,其中第二类最容易实现,以独立类库的方式引入就可以
对于第一类的改进,Restrotranslator使用ASM框架直接对字节码进行处理,语法,编译上的改进在字节码层面是不受影响的,实际上就是处理了字节码