类的生命周期

类从被加载到虚拟机内存开始到卸载出内存为止,其生命周期包括如下七个部分:

  • 加载 loading : 查找具有特定名称的类或者接口类型的二进制表示,并从该二进制表示创建类或者接口的过程。
  • 链接: 下辖三个阶段: 验证、准备、解析,是获取类或者接口并将其组合到java虚拟机的运行时状态以便可以执行的过程。
    • 验证 verification
    • 准备 preparation
    • 解析 resolution
  • 初始化 initialization: 执行类或者接口的初始化方法。
  • 使用 using
  • 卸载 unloading

类加载时机

java虚拟机没有规定类被加载的时机,但是确规定了类进行初始化的场景:
对于初始化阶段,虚拟机规范中定义了如下6中情况,必须对类进行初始化:

  1. 当遇到 new、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”), newInstance() 等等。如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。详见

类加载过程

加载

它是类加载的第一步,主要完成如下三件事:

  1. 通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流。除了从class文件中获取外,还可以从jar包中获取,从网络中获取(applet)以及其他文件生成(jsp)应用等。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个标识此类的java.lang.Class对象,作为方法区中,这个类的各种数据的访问入口。(一般而言,java类实例应当存放在堆中,但在hotspot中尽管Class是对象,但是被存放在方法区中)

加载过程主要是通过类加载器完成的。类加载器有多种,在加载类时,具体由哪一个类加载器加载是由双亲委派模型决定的(当然也可以破坏双亲委派机制)。
对于任意一个类,都需要由它的类加载器和这个类本身确定其在java虚拟机中的唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就必定不相等。这里的相等包括 代表类的Class对象的equals()|isAssignaleFrom()|isInstance()等方法的返回结果,也包括使用instanceof关键字对对象所属关系的判定结果。

每个java类都有一个引用指向加载它的类加载器 Classloader。注意有个例外,即数组类不是由类加载器加载的,而是由jvm通过 字节码 newarray创建的。数组类通过getClassloader获取ClassLoader时和该数组元素类型的ClassLoader是一致的。

相对于类加载的其他阶段,加载阶段(获取类的二进制字节流的过程)是可扩展性最强的阶段。开发人员既可以使用系统提供的类加载器来完成加载,也可以通过自定义类加载器(重写类加载器的loadClass方法)进而控制字节流的获取方式。(针对非数组对象)

加载阶段与连接阶段的部分动作(部分字节码文件格式校验)是交叉进行的,加载尚未结束时,连接过程可能已经开始了。

数组类的加载过程如下:

  1. 如果数组的组件类型是引用类型(非基础类型),则递归加载这个组件类型;
  2. 如果数组组件类型不是引用类型(int[]),则使用引导类加载器加载;
  3. 数组类的可见性与其他组件类型一致,如果组件类型不是引用类型,则将该数组的可见性设置为public。

验证

验证是连接阶段的第一步,其目的是确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束,以保证这些信息被当做代码运行后不会危害虚拟机自身的安全。
验证阶段在类加载过程中耗费的资源相对较多,但确有必要,可以有效防止恶意代码的执行。但验证阶段也不是必须要执行的阶段。如果程序运行的全部代码都已经被反复使用和验证过,在生产环境的实施阶段可以考虑使用Xverify:none来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

验证阶段包含如下四个过程:文件格式校验(Class文件格式检查)、 元数据验证(字节码语义检查)、字节码验证(程序语义检查)、符号引用验证(类的正确性检查)

  • 文件格式校验: 验证字节流是否符合Class文件格式规范,是否能够被的当前版本的虚拟机处理。比如是否以魔术0xCAFEBABE开头、主次版本是否在当前虚拟机处理范围内;常量池中的常量是否有不被支持的常量类型;指向常量的各种索引值中是否由指向不存在的常量或不符合类型的常量。
    文件格式校验这一过程基于该类的二进制字节流,主要目的是保证输入的字节流能够正确的解析(符合Class文件格式规范且能被当前虚拟机处理)且存储于方法区内。
    方法区中会存储 已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

  • 元数据验证: 保证不存在不符合java语言规范的元数据信息。比如 这个类是否有父类(除java.lang.Object外,所有类都应当有父类);这个类的父类是否继承了不允许被继承的父类(比如被final修饰的类);如果当前类不是抽象类,他是否实现了父类或者接口中要求实现的所有方法;类中字段和方法是否和父类相矛盾。

  • 字节码验证: 保证被校验的类的方法在运行时不会做出危害虚拟机的事。保证任意时刻操作数栈的数据类型和指令代码序列能配合工作。保证跳转指令不会跳转到方法体以外的字节码指令上;保障方法体中的类型转换是有效的。它是整个验证阶段中最耗时的步骤,尽管如此,也不能保障绝对安全。

  • 符号引用验证:符号引用验证的主要目的是确保 解析阶段能正常执行。 检查符号引用中使用字符串所描述的全限定名是否能找到对应的类;在对应类中是否存在着符合方法的字段描述以及简单名称所描述的方法和字段;在符号引用中的类、字段、方法的访问性是否可以被当前类访问。如果无法通过符号引用验证,将会抛出noSuchClass异常。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值,这些内存都将在方法区中分配。

准备阶段分配的内存仅包括被static修饰的类变量,实例对象则在对象实例化时被分配在堆内存中。

准备阶段一般是赋予数据类型的零值,特例是被 final 关键字修饰的常量,需要在准备阶段就赋予指定值。
java基础类型的零值如下:

解析

解析阶段是将虚拟机常量池内的符号引用转化为直接引用的过程。主要针对类、接口、字段、类方法、接口方法、方法句柄和调用限定符等7类符号引用。

符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存中的内容。 符号引用在Class文件中它以CONSTANT_Class_info| CONSTANT_Fieldref_info|CONSTANT_Methodref_info等类型的常量出现。
直接引用:是可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局直接相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不同。如果有了直接引用,则引用的目标必然已经存在于虚拟机内存中。

初始化

上述阶段,除了加载阶段可以通过用户自定义的类加载器加载,其余部分是由虚拟机主导,初始化阶段则由用户代码逻辑主导。
在准备阶段,类变量被赋予对应类型的初始零值,被final修饰的赋予真实值。在初始化阶段,还需要根据用户的程序代码,初始化实例变量。
前文已经描述了虚拟机规范中定义的必须对类进行初始化的六种场景,这里不再赘述。

换句话说,初始化阶段是执行类构造器< clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在静态语句中可以赋值,但不能访问。

    1
    2
    3
    4
    5
    6
    7
    8
    public class Test {
    static {
    i=0; //可以赋值
    System.out.print(i); //编译器会提示“非法向前引用”
    }
    static int i=1;
    }

  • <clinit>()与 实例构造器init()方法不同,它不需要显示调用父类构造器,由虚拟机保障在子类的<clinit>()方法执行前,父类的方法已经执行完毕。虚拟机中第一个被执行<clinit>()方法的类必是java.lang.Object

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Father {
    public static int a = 1;
    static {
    a = 2;
    }
    }
    class Child extends Father {
    public static int b = a;
    }
    public class ClinitTest {
    public static void main(String[] args) {
    System.out.println(Child.b);
    }
    }


    执行上面的代码,会打印出2,也就是说b的值被赋为了2。我们来看得到该结果的步骤。首先在准备阶段为类变量分配内存并设置类变量初始值,这样A和B均被赋值为默认值0,而后再在调用< clinit>()方法时给他们赋予程序中指定的值。当我们调用Child.b时,触发Child的< clinit>()方法,根据规则2,在此之前,要先执行完其父类Father的< clinit>()方法,又根据规则1,在执行< clinit>()方法时,需要按static语句或static变量赋值操作等在代码中出现的顺序来执行相关的static语句,因此当触发执行Father的< clinit>()方法时,会先将a赋值为1,再执行static语句块中语句,将a赋值为2,而后再执行Child类的< clinit>()方法,这样便会将b的赋值为2。 如果我们颠倒一下Father类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。很明显是根据规则1,执行Father的< clinit>()方法时,根据顺序先执行了static语句块中的内容,后执行了public static int a = 1;`语句。

  • <clinit>()对于类或者接口而言并不是必须的。如果一个类、接口没有静态代码块,也没有对类变量的赋值操作,或者该类声明了类变量,但没有明确使用类变量初始化语句或者静态初始化语句初始化或者该类经包含静态final变量的初始化语句,那么编译器可以
    不为这个类生成<clinit>()方法。

  • 接口中不能使用静态代码块,但仍有类变量初始化的赋值操作,故而会生成<clinit>()方法,但是与类不同的是,执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法,只有父接口中定义的变量被使用时,父接口才会被初始化。

  • 虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁和同步。如果多个线程初始化一个类,只会有一个线程会执行<clinit>()方法,其余线程会等待。

引用

1. 类加载过程详解
2. Chapter 5. Loading, Linking, and Initializing
3. Java JVM的类加载过程详解
4. jvm类加载器,类加载机制详解,看这一篇就够了
5. 类加载机制详解