Java类的初始化

前言

上一篇文章中我们讲到Java类加载机制,我们知道Java类加载需要经过装载、验证,准备,解析,初始化这几个阶段。但是类在什么时候会进行初始化呢?初始化的过程又是怎样的呢?这篇文章我们会简单的梳理一下。

类的初始化时机

什么时候需要开始类加载的第一个步骤“装载”。虚拟机规范中并没有强行约束,可以交给虚拟机的具体实现自由把握,但是对于类的初始化阶段,虚拟机是严格规定了有且只有5种情况必须立即对类进行“初始化“(而装载、验证、准备需要在此之前开始):

1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

4. 当虚拟机启动时,用户指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上这五种场景被称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发类的初始化,称为被动引用。

被动引用例一:通过子类引用父类的静态字段,不会导致子类初始化
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;
}


public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

public static void main(String[] args) {
        System.out.println(SubClass.value);
}

打印结果

SuperClass init!
123

从打印结果我们可以看出,子类引用父类的静态字段时,只会初始化父类,不会导致子类的初始化。

被动引用例二:通过数组定义来引用类,不会触发此类的初始化
public static void main(String[] args) {
    SuperClass[] superClassArray = new SuperClass[10];
}

运行后没有看到任何打印信息,说明没有触发SuperClass.class的初始化,但是这段代码里面触发了另外一个名为Lcom.blog.test.jvm.SuperClass的类的初始化阶段,对于用户来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。

这个类代表了一个元素类型为SuperClass.class的唯一数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为publiclenth属性和clone()方法)都实现在这个类里。Java 语言中对数组的访问比C/C相对安全是因为这个类封装了数组元素的访问方法,而C/C直接翻译为对数组指针的移动。在Java语言中,当检查到发生数组越界时会抛出java.lang.ArrayIndexOutOfBoundsException异常。

被动引用例三:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 123;

    public static final String CONST = "Hello";
}

public class PassiveReference {
    public static void main(String[] args) {
        System.out.println(SuperClass.CONST);
    }
}

打印结果

Hello

只打印了Hello,说明SuperClass类没有进行初始化。这是因为,虽然在Java源码中SuperClass类对CONST常量进行的引用,但其实在编译阶段,通过常量的传播优化,已经将此常量的值Hello存储到了PassiveReference类的常量池当中,以后PassiveReference类对常量SuperClass.CONST的引用,都会被转化为PassiveReference类对自身常量池的引用了,也就是说,实际上PassiveReference的Class文件之中并没有SuperClass类的符号引用入口,这两个类在编译成class之后就不存在任何联系了。

接口的加载过程和类加载过程稍有一些不同,针对接口需要做一些特殊说明:接口也有初始化过程,这点与类是一致的,上面的代码都是用静态代码块“static{}“来输出初始化信息的,而接口中不能使用“static{}“语句块,但编译器仍然会为接口生成“clinit()“类构造器,用于初始化接口中所定义的成员变量。接口与类真正有所区别的是:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

类初始化

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员制定的主观计划去初始化类变量和其他资源,或者从另一个角度来表达:初始化阶段是执行类构造器< clinit >()方法的过程。

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

public class SuperClass {
    static {
        i = 10;  // 给变量赋值可以正常编译通过
        System.out.println(i);  // 编译报错提示"Illegal forward reference"
    }
    public static int i;
}

< clinit >()方法与类的构造函数(或者说实例构造器< init >) 不同,它不需要显式地调用父类构造器,虚拟机会保证子类的< clinit >()方法执行之前,父类的< clinit >()方法已经执行完毕。因此在虚拟机中第一个被执行的< clinit >()方法的类肯定是java.lang.Object。

由于父类的< clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类地变量赋值操作。

< clinit >()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit >()方法。

这里我们可以做一个实验:含有静态变量但是没有赋值操作

public class Student{
    private static int age;
}

javac Student.java 将生成的Student.class用16进制编辑器打开

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 31 00 12 0A 00 03 00 0F 07    J~:>...1........
00000010: 00 10 07 00 11 01 00 03 61 67 65 01 00 01 49 01    ........age...I.
00000020: 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00    ..<init>...()V..
00000030: 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E 75 6D 62    .Code...LineNumb
00000040: 65 72 54 61 62 6C 65 01 00 12 4C 6F 63 61 6C 56    erTable...LocalV
00000050: 61 72 69 61 62 6C 65 54 61 62 6C 65 01 00 04 74    ariableTable...t
00000060: 68 69 73 01 00 22 4C 69 6E 6B 2F 7A 68 61 6F 6A    his.."Link/zhaoj
00000070: 75 6E 2F 63 6C 61 7A 7A 2F 6C 6F 61 64 65 72 2F    un/clazz/loader/
00000080: 53 74 75 64 65 6E 74 3B 01 00 0A 53 6F 75 72 63    Student;...Sourc
00000090: 65 46 69 6C 65 01 00 0C 53 74 75 64 65 6E 74 2E    eFile...Student.
000000a0: 6A 61 76 61 0C 00 06 00 07 01 00 20 69 6E 6B 2F    java........ink/
000000b0: 7A 68 61 6F 6A 75 6E 2F 63 6C 61 7A 7A 2F 6C 6F    zhaojun/clazz/lo
000000c0: 61 64 65 72 2F 53 74 75 64 65 6E 74 01 00 10 6A    ader/Student...j
000000d0: 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 00    ava/lang/Object.
000000e0: 21 00 02 00 03 00 00 00 01 00 0A 00 04 00 05 00    !...............
000000f0: 00 00 01 00 01 00 06 00 07 00 01 00 08 00 00 00    ................
00000100: 2F 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00    /........*7..1..
00000110: 00 02 00 09 00 00 00 06 00 01 00 00 00 03 00 0A    ................
00000120: 00 00 00 0C 00 01 00 00 00 05 00 0B 00 0C 00 00    ................
00000130: 00 01 00 0D 00 00 00 02 00 0E                      ..........

含有静态变量并且有赋值操作:

public class Teacher{
    private static int age = 10;
}

同样编译后,查看

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 31 00 15 0A 00 04 00 11 09    J~:>...1........
00000010: 00 03 00 12 07 00 13 07 00 14 01 00 03 61 67 65    .............age
00000020: 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03    ...I...<init>...
00000030: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E    ()V...Code...Lin
00000040: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C    eNumberTable...L
00000050: 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C    ocalVariableTabl
00000060: 65 01 00 04 74 68 69 73 01 00 22 4C 69 6E 6B 2F    e...this.."Link/
00000070: 7A 68 61 6F 6A 75 6E 2F 63 6C 61 7A 7A 2F 6C 6F    zhaojun/clazz/lo
00000080: 61 64 65 72 2F 54 65 61 63 68 65 72 3B 01 00 08    ader/Teacher;...
00000090: 3C 63 6C 69 6E 69 74 3E 01 00 0A 53 6F 75 72 63    <clinit>...Sourc
000000a0: 65 46 69 6C 65 01 00 0C 54 65 61 63 68 65 72 2E    eFile...Teacher.
000000b0: 6A 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00    java............
000000c0: 20 69 6E 6B 2F 7A 68 61 6F 6A 75 6E 2F 63 6C 61    .ink/zhaojun/cla
000000d0: 7A 7A 2F 6C 6F 61 64 65 72 2F 54 65 61 63 68 65    zz/loader/Teache
000000e0: 72 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62    r...java/lang/Ob
000000f0: 6A 65 63 74 00 21 00 03 00 04 00 00 00 01 00 0A    ject.!..........
00000100: 00 05 00 06 00 00 00 02 00 01 00 07 00 08 00 01    ................
00000110: 00 09 00 00 00 2F 00 01 00 01 00 00 00 05 2A B7    ...../........*7
00000120: 00 01 B1 00 00 00 02 00 0A 00 00 00 06 00 01 00    ..1.............
00000130: 00 00 03 00 0B 00 00 00 0C 00 01 00 00 00 05 00    ................
00000140: 0C 00 0D 00 00 00 08 00 0E 00 08 00 01 00 09 00    ................
00000150: 00 00 1E 00 01 00 00 00 00 00 06 10 0A B3 00 02    .............3..
00000160: B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00    1...............
00000170: 04 00 01 00 0F 00 00 00 02 00 10                   ...........

通过对比我们可以看到,只有对静态变量进行赋值操作,才会生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成< clinit >()方法。但接口与类不同的是,执行接口的< clinit >()方法不需要先执行父接口的< clinit >()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit >()方法。

虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的< clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。如果在一个类的< clinit >()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。需要注意的是,其他线程虽然会被阻塞,但如果执行< clinit >()方法的那条线程退出< clinit >()方法后,其他线程唤醒之后不会再次进入< clinit >()方法。同一个类加载器下,一个类型只会初始化一次。

参考《深入理解Java虚拟机》


Java类的初始化
https://www.zhaojun.inkhttps://www.zhaojun.ink/archives/javaclassinitialize
作者
卑微幻想家
发布于
2021-04-15
许可协议