java核心系列(三)—java类加载器

一,概览

类加载

1,类的生命周期

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

2,连接

链接过程:验证、准备和解析。

  1. 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
  2. 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。
  3. 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。

3,类的使用

类的使用包括主动引用和被动引用。

a,主动引用

通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
通过反射方式执行以上三种行为。
初始化子类的时候,会触发父类的初始化。
作为程序入口直接运行时(也就是直接调用main方法)。

b,被动引用

引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
定义类数组,不会引起类的初始化。
引用类的常量,不会引起类的初始化。

4,卸载

如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

二,问答

1,什么是双亲委派?

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

2,由不同的类加载器加载的指定类还是相同的类型吗?

在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。

3,在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

4,自定义类的默认父加载器是谁?

AppClassLoder

5,线程上下文类加载器

使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。

6,SPI

SPI(Service Provider Interface) 是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

7,如何在系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器中选择?

  1. 系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。
  2. 上下文类加载器要比当前类加载器更适合于框架编程。
  3. 当前类加载器则更适合于业务逻辑编程。

8,java中使用的类加载器

  1. JNDI使用线程上下文类加载器。
  2. Class.getResource()和Class.forName()使用当前类加载器。
  3. JAXP使用上下文类加载器。
  4. java.util.ResourceBundle使用调用者的当前类加载器。
  5. URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。
  6. Java序列化API缺省使用调用者当前的类加载器。

9,getResourceAsStream辨析

  1. Class.getResourceAsStream(String path) : path 不以’/‘开头时默认是从此类所在的包下取资源,以’/‘开头则是从
    ClassPath根下获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
  2. Class.getClassLoader.getResourceAsStream(String path) :默认则是从ClassPath根下获取,path不能以’/‘开头,最终是由ClassLoader获取资源。
  3. ServletContext.getResourceAsStream(String path):默认从WebAPP根目录下取资源,Tomcat下path是否以’/‘开头无所谓,当然这和具体的容器实现有关。

10,Web容器设计原则

  1. 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
  2. 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
  3. 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

11,tomcat的类加载规则

  1. 查找本地缓存
  2. 使用系统类加载器加载系统类
  3. 使用本地类加载器加载类
  4. 如果没有找到,使用父类加载器加载类

12,OSGi

OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。

三,参考资料

  1. 详解java类的生命周期
  2. 深入探讨 Java 类加载器
  3. 深入理解Java类加载器(1):Java类加载原理解析
  4. Tomcat源码解读系列(四)——Tomcat类加载机制概述