Java类的初始化

之前整理了《JVM之类加载机制》的文章,对于一个类的初始化阶段介绍太过简略,这里再开一篇文章,着重介绍类的初始化流程。

类初始化是类加载过程的最后一个阶段,到初始化阶段,才真正开始执行类中的Java程序代码。虚拟机规范严格规定了有且只有四种情况必须立即对类进行初始化

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

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

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

  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类,也就是main方法所在的类。

上面所说时显示的对象创建,还有几种隐式初始化的方式也说明一下:
  • 给String类型变量赋值时,若String对象在常量池中不存在,则创建一个新的String对象

  • 对String对象进行拼接操作,同上

  • 自动装箱机制可能会引起一个原子类型的包装类对象被创建。

这里再说一下类不被初始化的情况:
  • 对于静态字段(没有final修饰),只有直接定义这个字段的类才会被初始化,子类调用父类的静态字段并不会触发子类的初始化

  • static final 修饰的常量,在编辑时就存入了调用者的Class文件常量池中,调用时并不会触发定义类的初始化,也就是这个常量已经使用的类绑定。

  • 数组初始化过程并不会触发引用类的初始化

类或接口初始化(准备阶段)

编译器自动收集类变量赋值以及静态代码块后自动合并生成类的<clint>(),类开始初始化时会为static变量赋上零值。

  • <clint>()对于类和接口来说这个方法并不是必须的。
  • <clint>()中,静态语句只能访问定义在它之前定义的静态变量,定义在它之后的静态变量,可以赋值,但不能访问。
  • 子类<clint>()不需要显示的调用父类的构造器,JVM保证子类的<clint>()执行之前,父类的<clint>()已经执行完毕。
  • 由于父类的<clint>()先执行,所以父类的静态语句优先与子类的静态语句执行
  • 先对类,接口的执行<clint>()时并不需要执行父接口的<clint>()方法,只有使用父接口定义的变量时,父接口才会初始化。接口的实现类初始化时也不会调用接口的<clint>()
  • JVM保证一个类的<clint>()执行时线程安全的,多线程执行类的<clint>()时只能有一个被执行,其余线程等待(执行完毕后其他线程不再进入<clint>())。如果一个类的<clint>()执行耗时操作,可能会造成多进程阻塞

这里还有几个注意点:

接口也有初始化过程,在接口中不能使用“static{}”语句块,但编译器仍然会为接口生成<clint>(),用于初始化接口中定义的成员变量(实际上是static final修饰的全局常量)。

二者在初始化时最主要的区别是:当一个类在初始化时,要求其父类全部已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量),才会初始化该父接口。这点也与类初始化的情况很不同,调用类中的static final常量时并不会 触发该类的初始化,但是调用接口中的static final常量时便会触发该接口的初始化

类变量的赋值

下面简要说明下final、static、static final修饰的字段赋值的区别:

  • static修饰的字段在类加载过程中的准备阶段被初始化为0或null等默认值,而后在初始化阶段(触发类构造器<clint>())才会被赋予代码中设定的值,如果没有设定值,那么它的值就为默认值。

  • final修饰的字段在运行时被初始化(可以直接赋值,也可以在实例构造器中赋值),一旦赋值便不可更改;

  • static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,否则Javac时会报错。可以理解为在编译期即把结果放入了常量池中。

实例对象的初始化(初始化阶段)

编译器自动收集实例变量初始化以及实例代码块后自动合并生成类的<init>()

  • 子类初始化时会先调用父类<init>(),用以保证子类能正常初始化。
  • 执行子类的<init>()

那么从上面可以知道,一个类在初始化过程中,构造方法的执行过程如下:

  • 父类的<clint>()
  • 子类的<clint>()
  • 父类的<init>()
  • 子类的<init>()

一个子类自身代码的初始化执行顺序如下:

  • Static Field Initial (类变量)

  • Static Patch Initial (静态初始化块)

  • Field Initial (成员变量)

  • Field Patch Initial (初始化块)

  • Structure Initial (构造器)

上面第一条和第二条依据代码定义的顺序不同,执行的顺序也不同(定义在静态代码块之后的的类变量可以被静态代码块赋值,但是不能被访问)

举个栗子验证一下

public class ClassInitTest {

    public static void main(String[] args) {
        new Child();
    }

    public static class Parent {
        static {
            System.out.println("Parent static code");
        }

        public Parent() {
            System.out.println("Parent constructor code");
        }
    }

    public static class Child extends Parent {
    
         private int mInt = 100;

        {
            System.out.println(mInt);
            mInt = 200;
            System.out.println(mInt);
        }
    
        static {
            System.out.println("Child static code");
        }

        public Child() {
            System.out.println("Child constructor code");
            System.out.println(mInt);
        }
    }
}

打印如下:

Parent static code
Child static code
Parent constructor code
100
200
Child constructor code
200

上面可以看到即使类变量定义在成语变量和初始代码块之下也是先被执行的,同时我们可以看到构造器的代码时最后执行的。

你可能感兴趣的:(Java类的初始化)