Java类加载器

Java类加载器

一、类与类加载器

类加载器用于实现类的加载动作,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

二、双亲委派模型

站在Java虚拟机的角度来看,只存在俩种不同的类加载器:一种是启动类加载器(bootstrap classloader),这个类加载器是同c++语言实现(hotspot虚拟机),是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都是由Java语言实现,独立存在于虚拟机外部,并且全部都继承自抽象类java.lang.classloader。

在开发人员的角度,自jdk1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构。

启动类加载器

这个类加载器负责加载存放在java_home/lib目录,或者被-Xbootclasspath参数指定的路径存放的,而且是java虚拟机能够识别的(按照文件名识别)类库加载到虚拟机的内存中。

启动类加载器无法由Java程序直接引用,用户编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那么直接使用null代替即可。

扩展类加载器

这个类加载器是在类sun.misc.launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载java_home/lib/ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。这是Java系统类库的扩展机制。

应用程序类加载器

这个类加载器是由sun.misc.launcher$AppClassLoader来实现(系统类加载器),它负责加载用户类路径上所有的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承的关系来实现的,而是通常使用组合关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求是,子加载器才会尝试自己去加载。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如object类。

三、破坏双亲委派模型

双亲委派模型第一次被破坏时为了面对已经存在的用户自定义类加载的代码。

第二次是由这个模型自身的缺陷导致的,双亲委派模型很好的解决了各个类加载器协作时基础类的一致性问题。但是有基础类又要调用回用户的代码,那该怎么办。一个典型的例子是JNDI服务,它的代码由启动类来完成加载,但是JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的classpath下的JNDI服务提供者接口(SPI)的代码。

为了解决这个问题,Java虚拟机引入了:线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContext-Classloader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果应用程序的全局范围都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,这是一种父类加载器去请求子类加载器完成类加载行为。Java中涉及SPI的加载基本都是采用这种方式来完成,例如JDNI、JDBC、JCE。当SPI的服务提供者多于一个的时候,代码就只能根据具体的具体的类型来硬编码判断,为了消除这个不优雅的实现方式,在JDK6时,jdk提供了java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式。

第三次破坏是由于用户对程序动态性的追求而导致的。例如:代码热替换,模块热部署等OSGI。

四、Java模块化系统

在jdk9引入的Java模块化系统是对java技术的一次重要升级,为了能够实现模块化可配置封装隔离机制的目标。Java虚拟机对类加载架构也做出了相应的变动调整。

Java模块定义包含:

  • 依赖其他模块的列表
  • 导出的包的列表,即其他模块可以使用的列表
  • 开放的包的列表,即其他模块可反射访问模块的列表
  • 使用的服务列表
  • 提供服务的实现列表

可配置的封装隔离机制首先要解决jdk9之前基于类路径来查找依赖的可靠性问题,可提前至启动阶段发现问题。还解决了原来类路径上跨JAR文件的public类型的可访问性问题。

为了使可配置的封装隔离机制能够兼容传统的类路径查找机制,jdk9提出了与类路径相对应的模块路径。简单来说,就是某个类库到底是模块还是传统的jar包,只取决于它存放在那种路径上。规则如下:

jar文件在类路径的访问规则:所有类路径下的jar文件及其他资源文件,都视为自动打包在一个匿名模块里面。这个匿名模块几乎没有任何隔离的。它可以看到和使用类路径上所有的包、jdk系统模块中所有导出的包,以及模块路径上所有模块导出的包。

模块在模块路径的访问规则:模块路径下的具名模块只能访问到它依赖定义中列名依赖的依赖包和模块,匿名模块里所有的内容对具名模块来说都是不可见的。

jar文件在模块路径的访问规则:如果把一个传统的,不包含模块定义的jar文件放在模块路径中,它就会变成一个自动模块。自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包。

模块化下的类加载器发生的变动:

扩展类加载器被平台类加载器取代。整个jdk都基于模块化构建,java类库已经天然满足了可扩展的需求。

其次,平台类加载器和应用程序类加载器都不在派生自java.net.URLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.inernal.loader.BuiltinClassLoader。

最后,jdk9中虽然维持着三层类加载器和双亲委派的架构,但类加载委派的关系也发生了变化。当平台及应用程序类加载器收到类加载请求,在委派给父加载器之前,要先判断该类是否能够归属到某一个系统模块中,如果可以,就要优先委派给负责那个模块的加载器完成加载。

 

参考:

《深入理解Java虚拟机》周志明

 

posted @ 2022-01-06 10:06  刘姥爷观园子  阅读(139)  评论(0编辑  收藏  举报