类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

类加载过程

虚拟机加载 Class 文件的过程:加载连接初始化。连接过程:验证准备解析。

加载

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

验证

加载阶段和连接阶段的部分内容是较差进行的,加载阶段尚未结束,连接阶段就可能已经开始了。

四件事:

  1. 文件格式验证。验证字节流是否符合 Class 文件格式的规范,如常量池的常量是否有不支持的类型,是否以 0xCAFEBABE 开头,主次版本号是否在当前虚拟机的处理范围内。
  2. 元数据验证。对字节码描述的信息进行语义分析,以保证描述的信息符合 Java 语言规范的要求,如该类继承了不允许继承的类(final)。
  3. 字节码验证。最复杂的一个阶段,通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如保证任意时刻操作数栈和指令代码序列都能配合工作。
  4. 符号引用验证。确保解析动作正确执行。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。需要注意:

  1. 仅对类变量进行内存分配,即静态变量,不包含实例变量。
  2. JDK7 之后,字符串常量池、静态变量等移动到堆中,即类变量在 Java 堆中分配内存。
  3. 类变量设置初始值时,设置为基本类型的初始值,除非加上 final 关键字,则直接被赋予值。如 static int value=111,准备阶段初始值为 0,初始化阶段再赋值 111; static final int value = 111,在准备阶段就直接赋值 111.

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,就是得到类或字段、方法在内存中的指针或偏移量。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用。

符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用方法时,只需要知道这个方法在放发表的偏移量就可以调用该方法了。通过解析符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

初始化

初始化阶段是执行初始化方法 <clinit>() 方法(编译后自动生成的)的过程,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
<clinit>() 方法时带锁线程安全的,以此来保证 JVM 在多线程环境中的安全性,但也会使得多线程环境下进行类初始化的话可能引起多个进程阻塞,并且难以发现。

类/接口初始化的 6 种情况:

  1. 遇到 new (创建类实例) 、 getstatic (访问类静态变量) 、 putstatic (给静态变量赋值) 或 invokestatic (调用类静态方法) 这 4 条直接码指令时,初始化类。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。
  3. 初始化子类时,父类未初始化,则先初始化父类。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类(包含 main 方法的类),虚拟机初始化该类。
  5. 使用 MethodHandleVarHandle 轻量级的反射调用机制,需要使用 findStaticVarHandle 来初始化要调用的类。
  6. 接口中定义了 JDK8 新引入的默认方法,实现类初始化前,要先初始化接口。

卸载

卸载,即类的 Class 对象被 GC。卸载类需满足 3 个条件:

  1. 该类的所有实例对象都被 GC,堆中不存在该类的实例对象。
  2. 该类没有在其他地方被引用。
  3. 该类的类加载器的实例被 GC。

因此,JVM 生命周期内,JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器可能被卸载。

类加载器

一个非数组类的加载阶段是可控性最强的阶段,可以自定义类加载器去控制字节流的获取方式。数组类型不通过类加载器创建,由 Java 虚拟机直接创建。所有的类都有类加载器加载,加载的作用就是将.class 文件加载到内存中。

JVM 内置的类加载器

JVM 内置了三个 ClassLoader,其中 BootstrapClassLoader 类加载器由 C++实现,其余由 Java 实现并继承自 java.lang.ClassLoader

  1. BootstrapClassLoader :启动类加载器,最顶层的加载类,C++实现,负责加载 %JAVA_HOME%/lib 目录下的 jar 包和类或者被 -Xbootclasspath 参数指定的路径中的所有类。
  2. ExtensionClassLoader :扩展类加载器,主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  3. AppClassLoader :应用程序类加载器,面向用户的加载器,负责加载当前应用 classpath 下的 jar 包和类。

双亲委派模型简介

每个类都有一个对应的类加载器,系统中的 ClassLoader 在协同时默认使用双亲委派模型。

在类加载时,判断当前类是否被加载过,已被加载的类直接返回,否则才会尝试加载。

加载时,首先会先把请求委派给父类加载器的 loadClass() 处理,因此所有的请求都会被传到顶层的启动类加载器处理。当父类加载器无法处理时,才会由自己处理。当父类加载器为 null 时,会使用启动类加载器作为父类加载器。

用户自定义类加载 - 应用程序类加载器 - 扩展类加载器 - 启动类加载器。

自底向上检查类是否被加载,自顶向下尝试加载类。

双亲委派模型的好处

双亲委派模型保证了 Java 程序的稳定运行,避免类的重复加载。同时,这保证了 Java 的核心 API 不被篡改。

JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类。

自定义类加载器与不使用双亲委派模型

自定义类加载器
继承 ClassLoader 类,并重写 findClass()方法。无法被父类加载器加载的类会通过这个方法被加载。

不使用双亲委派模型
重写 loadClass()方法。

打破双亲委派机制的例子,为什么打破

  • JNDI 通过引入线程上下文类加载器,可以在 Thread. setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
  • Tomcat 中可以部署多个 web 项目,为了保证每个 web 项目互相独立,所以不能都由 AppClassLoader 加载,所以自定义了类加载器 WebappClassLoader,WebappClassLoader 继承自 URLClassLoader,重写了 findClass 和 loadClass,并且 WebappClassLoader 的父类加载器设置为 AppClassLoader。
    • WebappClassLoader. loadClass 中会先在缓存中查看类是否加载过,没有加载,就交给 ExtClassLoader,ExtClassLoader 再交给 BootstrapClassLoader 加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由 AppClassLoader 递归加载。
  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。tomcat 之所以造了一堆自己的 classloader,大致是出于下面三类目的:
    • 对于各个 webapp 中的 classlib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的 lib 以便不浪费资源。
    • jvm 一样的安全性问题。使用单独的 classloader 去装载 tomcat 自身的类库,以免其他恶意或无意的破坏;
    • 热部署。
  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。