Skip to content

类的生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程。
Alt text

1. 开发调试准备

1.1 打印类加载日志

对于JDK8和更低版本:-XX:+TraceClassLoading
对于JDK9和更高版本:-Xlog:class+load=info

1.2 查看内存中的对象

JDK自带的hsdb工具,hsdb工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中:

sh
java -cp sa-jdi.jar sun.jvm.hotspot.HSDB

Alt text 启动后点击File-Attach Hotspot process, 然后填写java程序的进程号ID,点击OK后,点击Tools中的Object Inspect后查看,查找我们的AppMainDemo类:
Alt text 可以看到在JDK8中静态常量是保存在Class类对象上面的。

2. Loading(装载)阶段

第一步是类加载器根据类的全限定名通过不同的渠道(比如本地文件、动态代理生成、通过网络传输的类)以二进制流的方式获取字节码信息。
第二步是解析类的二进制数据流为方法区(只是JVM规范中的一个概念并不真实存在,早期的JDK将字节码存在永久代里,现在改为元空间中存)内的数据结构(类模板对象InstanceKlass),JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
第三步根据方法区内的数据结构会在堆中创建一个java.lang.Class对象,每个类都对应有一个Class类型的对象, 外部可以通过访问代表类的Class对象来获取的类在方法区的数据结构(就是实现反射的提供入口),同时在JDK8之后静态字段的数据只放在堆的Class对象上可以提供访问。

为何方法区和堆区都有类信息?

这样子设计是因为方法区中所有信息是通过JVM的C++语言维护的,Java开发者使用起来不方便,同时通过访问堆中的Class对象也能起到能很好地控制开发者访问数据的范围。

3. Linking(链接)阶段

2.1 验证阶段(Verification)

它的目的是保证加载的字节码是合法、合理并符合JVM规范的。验证的步骤如下:

  1. 格式验证:是否以魔数OxCAFEBABE开头,主版本和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。
  2. Java虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。
  3. Java虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。
  4. 校验器还将进行符号引用的验证。

2.2 准备阶段(Preparation)

准备阶段会给静态变量赋默认初始值。Java虚拟机为各类型变量默认的初始值如表所示:

Alt text 但如果是final修饰的静态常量会被显式赋值。

2.3 解析阶段(Resolution)

主要是将常量池中的符号引用替换为直接引用。

提示

  1. 符号引用就是在字节码文件中使用编号来访问常量池中的内容。
  2. 直接引用就是不再使用编号,而是使用内存中地址进行访问具体的数据。

3. Initialization(初始化)阶段

初始化阶段如果被触发,初始化阶段会执行类的初始化方法:<clinit>()方法。该方法仅能由Java编译器生成并由JVM调用。而只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。特别的只包含static final修饰的基本数据类型的字段不会生成<clinit>()方法。

3.1 初始化的顺序

在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的<clinit>总是在子类<clinit>之前被调用。也就是说,父类的static块优先级高于子类。
在一个类中clinit方法中的执行顺序与Java中编写的顺序是一致的:
Alt text

3.2 触发初始化

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
  2. 调用Class.forName(String className)。
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。

3.3 不会触发初始化

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。比如当通过子类引用父类的静态变量,不会导致子类初始化。
  2. 通过数组定义类引用,不会触发此类的初始化。
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
  4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

4. 类的Using(使用)

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便"万事俱备,只欠东风",就等着开发者使用了。
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。

5. 类的Unloading(卸载)

(1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)
(2) 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。
(3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)。
综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能。