《深入了解Java虚拟机》第九章

第九章 类加载及执行子系统的案例与实战

Class文件何种形式存储,类型何时加载、如何连接,虚拟机如何执行指令都是由虚拟机直接控制的行为。能通过程序操作的 ,主要是字节码生成类加载器两部分功能。

9.2 案例分析

9.2.1 Tomcat:正统的类加载器架构

· 主流的Java Web服务器,都实现了自定义的类加载器,且一般不止一个。
· 一个功能健全的Web服务器,都要解决这些问题:

  1. 部署在同一服务器上的两个web应用程序所使用的Java类库可以实现相互隔离
    两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求每个类库在一个服务器只能有一份,服务器应当能保证两个独立应用程序的类库可以互相独立使用
  2. 部署在同一个服务器上的两个web应用程序所使用的类库可以共享
  3. 服务器需要尽可能保证自身安全不受部署的web应用程序的影响。因为目前很多服务器也是使用java实现的,有自身的类库依赖,需要与应用程序的类库相互独立。
  4. 支持JSP应用的Web服务器,基本都需要支持HotSwap功能
    JSP文件最终要编译成Java的Class文件才能被虚拟机执行,但JSP文件由于其纯文本存储的特性,运行期被修改的概率远远大于第三方类库或程序自己的class文件。PHP、JSP等网页应用也把修改后不需要重启作为很大的优势。
    因此,主流的web服务器都会支持JSP生成类的热替换。

· 由于存在上述问题,在部署web应用时,单独一个ClassPath不够。各种服务器都提供了许多类路径(多以lib,classes命名),来存放不同访问范围和服务对象的第三方类库。
· 通常每个目录下有一个对应的自定义类加载器,去加载放置在里面的java类库。

以Tomcat为例
Tomcat目录结构中,可以设置三组目录(/common/、/server/和/shared/,但默认不一定是开放的,可能只有/lib/目录存在)用于放java类库,还有一个存放web应用程序自身的“/WEB-INF/*”目录,一共4组。把Java类库放置在这四组目录中:

  • 放置在/common目录中。类库可被Tomcat和所有的web应用程序共同使用。
  • 放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见。
  • 放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中。类库仅可被该web应用程序使用,对Tomcat和其他web程序不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,按照双亲委派模型来实现。
下图是Tomcat 6以前默认的类加载器结构:
在这里插入图片描述

  • 最上面三个是JDK默认提供的类加载器。下面是Tomcat自己定义的类加载器:

    类加载器 加载的Java类库的目录
    Common类加载器 /common/*
    Catalina类加载器(也称为Server类加载器) /server/*
    Shared类加载器 /shared/*
    Webapp类加载器 /WebApp/WEB-INF/

    · 其中WebApp类加载器JSP类加载器通常还会存在多个实例,每个Web应用程序/JSP文件对应一个WebApp类加载器/JasperLoader类加载器。
    (一对多关系)

  • Common类加载器能加载的类都可以被Catalina和Shared类加载器使用。

  • Catalina和Shared类加载器自己加载的类是相互隔离的。

  • WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebApp类加载器实例之间相互隔离。

  • JasperLoader的加载范围只是这个JSP文件所编译出来的那一个Class文件。
    存在目的就是为了被丢弃:当服务器检测到JSP文件被修改,会替换掉目前的JasperLoader实例,并再建立一个新的JSP类加载器来实现JSP文件的热交换功能

· Tomcat 6及之后,简化了默认的目录结构。

  • 只有指定了tomcat/conf/catalina.properties配置文件的server.loadershare.loader项后,才会真正建立起Catalina类加载器和Shared类加载器的实例。
  • 否则需要用到这两个类加载器的地方都用Common类加载器的实例代替。
  • 默认配置文件中没有设置这两项,所以Tomcat 6及之后把/common、/server和/shared这3个目录默认合并到一起变成1个/lib目录。它相对于以前/common目录中类库的作用。

9.2.2 OSGi:灵活的类加载器架构

· OSGi是一个基于Java语言的动态模块化规范。现多用于智慧城市、智慧农业等地方。Eclipse IDE也是基于OSGi规范实现的。
· OSGi的每个模块(Bundle)与普通的Java类库区别不大,都以JAR格式进行封装。内部存储的都是Java的Package和Class。但一个Bundle可以声明它所依赖的包(通过Import-Package描述),也可以声明它允许导出的包(Export-Package)。
· 一个模块只有被被Export过的Package才可能被外界访问,其他的Package和Class将会被隐藏起来。
· 在OSGi里面,Bundle间的依赖关系,从上层模块依赖底层模块转变为平级模块间的依赖。

· 以上静态的模块化特性原本也是OSGi核心追求之一,但由于与后来出现的java的模块化系统(JPMS)重叠了,所以现在主要发展方向是动态模块化系统。今天引入OSGi主要是为了实现模块级的热插拔功能。譬如Eclipse中的安装、卸载、更新插件不需要重写启动。

· OSGi有以上诱人特点,必须归功于它灵活的类加载器架构。OSGi的Bundle之间,只有规则,没有固定的委派关系
· 在不涉及到某具体的Package时,所有Bundle都是平级关系。
· 只有具体到使用某个Package时和Class时,才会根据Package导入导出定义来构造Bundle间的委派与依赖。
· 另外,当一个Bundle提供服务时,要根据Export-Package列表严格控制访问范围。如果一个类存在于类库中,但没有被Export,那这个Bundle类加载器可以找到这个类,但是不会提供给别的类使用,OSGi也不会把其他Bundle的类加载请求委派给它。

· 在OSGi里,类加载时可能进行的查找规则如下:

  1. java.*开头的类,委派给父类加载器加载。
  2. 否则,委派列表名单内的类,委派给父类加载器加载。
  3. 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
  5. 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  7. 否则,类查找失败

· OSGi中,加载器之间的关系为网状结构。虽然灵活性更高,但也有新的隐患。如经常产生死锁
· 例如:Bundle A依赖Bundle B的Package B,而Bundle B又依赖了Bundle A的Package A
· Bundle A加载Package B里的类时,首先需要锁定当前类加载器的实例对象(ClassLoader.loadClass()是一个同步方法),然后把请求委派给Bundle B的加载器处理。这时候如果Bundle B也想加载Package A的类,那就会锁定自己的加载器再去请求Bundle A的加载器处理。就会产生死锁。
· Equinox提供了一个以牺牲性能为代价的解决方案:用户可以启用osgi.classloader.singleThreadLoads参数,按照单线程串行化的方式强制进行类加载操作。
· JDK7时出现JDK层面的解决方案,升级了类加载器架构,在
ClassLoader中增加了registerAsParallelCapable方法,对可并行的类加载进行注册声明,把锁的级别从ClassLoader对象本身,降低为要加载的类名这个级别。从底层避免以上这类死锁问题的出现。
· 换句话说,只要多线程加载的不是同一个类的话,loadClass方法都不会锁住。
注:依旧以上面的死锁为例。Bundle A中类C中想要加载Package B的类G,Bundle B中类D想要加载Package A的类F。