类或接口的初始化过程就是执行它们的初始化方法 <clinit>
。这个方法是由编译器在编译的时候生成到class文件中的,包含类静态field赋值指令和静态语句块(static{})中的代码指令两部分,顺序和源码中的顺序相同。
以下情况下,会触发类(用C表示)的初始化:
new(创建对象) , getstatic(获取类field) , putstatic(给类field赋值) , 或 invokestatic(调用类方法) 指令执行,创建C的实例,获取/设置C的静态字段,调用C的静态方法。
如果获取的类field是带有ConstantValue属性的常量,不会触发初始化
第一次调用 java.lang.invoke.MethodHandle
实例返回了 REF_getStatic
, REF_putStatic
, REF_invokeStatic
, REF_newInvokeSpecial
类型的方法句柄。
反射调用,如Class 或
java.lang.reflect`包中的类
如果C是一个类,它的子类 <clinit>
方法调用前,先调用C的 <clinit>
方法
如果C是一个接口,并且定义了一个非 abstract
, 非 static
的方法, 它的实现类(直接或间接)执行初始化方法 <clinit>
时会先初始化C.
C作为主类(包含main方法)时
可以看出在static{}中执行一些耗时的操作会导致类初始化阻塞甚至失败
为了加快初始化效率,jvm是多线程执行初始化操作的,可能会有多个线程同一时刻尝试初始化类,也可能一个类初始化过程中又触发递归初始化该类,所以jvm需要保证只有一个线程去进行初始化动作,jvm通过为已验证过的类保持一个状态和一个互斥锁来保证初始化过程是线程安全的。
虚拟机中类的状态:
实际上,虚拟机为类定义的状态可能不止上面4种,如hotspot,见 前文
除了状态,在初始化一个类之前,先要获得与这个类相关联的锁对象(监视器),记作LC。
类或接口C的初始化流程如下(jvm1.8规范):
等待获取C的锁LC.
如果C正在被其他线程初始化, 释放LC,并阻塞当前线程直到C初始化完成.
线程中断对初始化过程没有影响
如果C正在被当前线程初始化, 则肯定是在递归初始化时又触发C初始化. 释放LC并正常返回.
如果C的状态为已经初始化,释放LC并正常返回.
如果C的状态为初始化失败,释放LC并抛出一个 NoClassDefFoundError
异常.
否则记录当前类C的状态为初始化中,并设置当前线程为初始化线程, 然后释放LC.
然后, 按照字节码文件中的顺序初始化C中每个带有 ConstantValue
属性的 final
static
字段.
**注意:**jvm规范把常量的赋值定义在初始化阶段, <clinit>
执行之前,具体实现未必严格遵守。如hotspot虚拟机在解析字节码过程创建 _java_mirror
镜像类时已为每个常量字段赋值。
下一步, 如果C是一个类, 而且它的父类还未初始化, SC记作它的父类, SI1, ..., SIn
记作C实现的至少包含一个非抽象,非静态方法的接口(直接或间接的) 。 先初始化SC,所有父接口的顺序按照递归的顺序而不是继承层次的顺序确定, 对于一个被C直接实现的接口I (按照C的接口列表 interfaces
的顺序), 在I初始化之前,先循环遍历初始化I的父接口 (按照I的接口列表 interfaces
的顺序) .
下一步, 查看定义类加载器是否开启了断言(用于调试).
// ClassLoader // 查询类是否开启了断言 // 通过#setClassAssertionStatus(String, boolean)/#setPackageAssertionStatus(String, boolean)/#setDefaultAssertionStatus(boolean)设置断言 boolean desiredAssertionStatus(String className); 复制代码
下一步,执行C的初始化方法 <clinit>
.
如果C的初始化正常完成, 获取LC并将C的状态标记为已完成初始化, 唤醒所有等待线程,释放锁LC,初始化过程完成.
否则, 初始化方法必须抛出一个异常E. 如果E不是 Error
或其子类, 创建一个 ExceptionInInitializerError
实例(以E作为参数), 在接下来的步骤中,以这个实例替换E,如果因为内存溢出无法创建 ExceptionInInitializerError
实例,用一个 OutOfMemoryError
替换E.
获取 LC
, 标记C的初始化状态为发生错误, 通知所有等待线程, 释放 LC
, 并通过E或其他替代(见前一步)异常返回.
虚拟机的实现可能优化这个过程,在它可以判断初始化已经完成时, 取消在第1步获取锁 (和在第 4/5释放锁) , 前提是, 根据java内存模型, 所有的 happens-before 关系在加锁和优化锁时都存在.
接下来看一个例子:
interface IA { Object o = new Object(); } abstract class Base { static { System.out.println("Base <clinit> invoked"); } public Base() { System.out.println("Base <init> invoked"); } { System.out.println("Base normal block invoked"); } } class Sub extends Base implements IA { static { System.out.println("Sub <clinit> invoked"); } { System.out.println("Sub normal block invoked"); } public Sub() { System.out.println("Sub <init> invoked"); } } public class TestInitialization { public static void main(String[] args) { new Sub(); } } 复制代码
在hotspot虚拟机上运行:
javac TestInitialization.java && java TestInitialization 复制代码
可以看出初始化顺序为: 父类静态构造器 -> 子类静态构造块 -> 父类普通构造块 -> 父类构造器 -> 子类普通构造快 -> 子类构造器
,且普通构造快在实例构造器之前调用,与顺序无关。
关于接口由于没法添加static{},可以通过反编译看下也生成了 <clinit>
方法:
如果没有为类定义实例构造器,编译器会生成一个不带参数的默认构造器,里边调用父类的默认构造器
如果类中没有静态变量的赋值语句或静态代码块,则不必生成 <clinit>
最后,介绍几个相关面试题:
下面代码输出什么?
public class InitializationQuestion1 { private static InitializationQuestion1 q = new InitializationQuestion1(); private static int a; private static int b = 0; public InitializationQuestion1() { a++; b++; } public static void main(String[] args) { System.out.println(InitializationQuestion1.a); System.out.println(InitializationQuestion1.b); } } 复制代码
把q声明放到b后面呢?输出什么?
下面代码输出什么?
abstract class Parent { static int a = 10; static { System.out.println("Parent init"); } } class Child extends Parent { static { System.out.println("Child init"); } } public class InitializationQuestion2 { public static void main(String[] args) { System.out.println(Child.a); } } 复制代码
改成下面试试:
abstract class Parent { static final int a = 10; static { System.out.println("Parent init"); } } 复制代码
再改成下面这样试试:
abstract class Parent { static final int a = value(); static { System.out.println("Parent init"); } static int value(){ return 10; } } 复制代码