java类加载之初始化过程(附面试题)

类或接口的初始化过程就是执行它们的初始化方法<clinit>。这个方法是由编译器在编译的时候生成到class文件中的,包含类静态field赋值指令和静态语句块(static{})中的代码指令两部分,顺序和源码中的顺序相同。java

如下状况下,会触发类(用C表示)的初始化:面试

  • new(建立对象), getstatic(获取类field), putstatic(给类field赋值), 或 invokestatic(调用类方法) 指令执行,建立C的实例,获取/设置C的静态字段,调用C的静态方法。安全

    若是获取的类field是带有ConstantValue属性的常量,不会触发初始化bash

  • 第一次调用 java.lang.invoke.MethodHandle 实例返回了REF_getStatic,REF_putStatic, REF_invokeStatic, REF_newInvokeSpecial类型的方法句柄。多线程

  • 反射调用,如Classjava.lang.reflect`包中的类app

  • 若是C是一个类,它的子类<clinit>方法调用前,先调用C的<clinit>方法jvm

  • 若是C是一个接口,而且定义了一个非abstract, 非static 的方法, 它的实现类(直接或间接)执行初始化方法<clinit>时会先初始化C.优化

  • C做为主类(包含main方法)时spa

能够看出在static{}中执行一些耗时的操做会致使类初始化阻塞甚至失败线程

在类初始化以前,会先进行连接操做

为了加快初始化效率,jvm是多线程执行初始化操做的,可能会有多个线程同一时刻尝试初始化类,也可能一个类初始化过程当中又触发递归初始化该类,因此jvm须要保证只有一个线程去进行初始化动做,jvm经过为已验证过的类保持一个状态和一个互斥锁来保证初始化过程是线程安全的。

虚拟机中类的状态:

  • 类已验证和准备,但未初始化
  • 类正在被一个线程初始化
  • 类已经完成初始化,可使用了
  • 类初始化失败

实际上,虚拟机为类定义的状态可能不止上面4种,如hotspot,见前文

除了状态,在初始化一个类以前,先要得到与这个类相关联的锁对象(监视器),记做LC。

类或接口C的初始化流程以下(jvm1.8规范):

  1. 等待获取C的锁LC.

  2. 若是C正在被其余线程初始化, 释放LC,并阻塞当前线程直到C初始化完成.

    线程中断对初始化过程没有影响

  3. 若是C正在被当前线程初始化, 则确定是在递归初始化时又触发C初始化. 释放LC并正常返回.

  4. 若是C的状态为已经初始化,释放LC并正常返回.

  5. 若是C的状态为初始化失败,释放LC并抛出一个 NoClassDefFoundError异常.

  6. 不然记录当前类C的状态为初始化中,并设置当前线程为初始化线程, 而后释放LC.

    而后, 按照字节码文件中的顺序初始化C中每一个带有ConstantValue属性的 final static 字段.

    **注意:**jvm规范把常量的赋值定义在初始化阶段,<clinit>执行以前,具体实现未必严格遵照。如hotspot虚拟机在解析字节码过程建立_java_mirror镜像类时已为每一个常量字段赋值。

  7. 下一步, 若是C是一个类, 并且它的父类还未初始化, SC记做它的父类, SI1, ..., SIn 记做C实现的至少包含一个非抽象,非静态方法的接口(直接或间接的) 。 先初始化SC,全部父接口的顺序按照递归的顺序而不是继承层次的顺序肯定, 对于一个被C直接实现的接口I (按照C的接口列表 interfaces 的顺序), 在I初始化以前,先循环遍历初始化I的父接口 (按照I的接口列表 interfaces 的顺序) .

  8. 下一步, 查看定义类加载器是否开启了断言(用于调试).

    // ClassLoader
    
    // 查询类是否开启了断言
    // 经过#setClassAssertionStatus(String, boolean)/#setPackageAssertionStatus(String, boolean)/#setDefaultAssertionStatus(boolean)设置断言
    boolean desiredAssertionStatus(String className);
    复制代码
  9. 下一步,执行C的初始化方法<clinit>.

  10. 若是C的初始化正常完成, 获取LC并将C的状态标记为已完成初始化, 唤醒全部等待线程,释放锁LC,初始化过程完成.

  11. 不然, 初始化方法必须抛出一个异常E. 若是E不是 Error 或其子类, 建立一个 ExceptionInInitializerError 实例(以E做为参数), 在接下来的步骤中,以这个实例替换E,若是由于内存溢出没法建立 ExceptionInInitializerError 实例,用一个 OutOfMemoryError 替换E.

  12. 获取 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>

最后,介绍几个相关面试题:

  1. 下面代码输出什么?

    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后面呢?输出什么?

  2. 下面代码输出什么?

    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;
        }
    }
    复制代码

相关文章
相关标签/搜索