Java类初始化顺序

Java加载过程中,变量,构造器等是何时调用初始化的

Posted by fyypumpkin on August 7, 2018

前言

Java编写中,一定会遇到一些初始化顺序的问题,静态变量,静态初始化块,普通成员变量,普通初始化块以及构造器,本文将会使用例子的方式,来详细展示一下 java 类里面各个部分的初始化顺序以及不触发初始化的一些情况

正文

我先上代码,后面会详细解释

测试代码

没有继承,并且会触发类的初始化


public class NoExtendTest {
    public static String staticField = "静态变量";

    public String normalField = "普通成员变量";

    static {
        System.out.println(staticField);
        System.out.println("静态初始化块");
    }

    {
        System.out.println(normalField);
        System.out.println("普通初始化块");
    }

    public NoExtendTest(){
        System.out.println("构造器");
    }

}

class Main {
    public static void main(String[] args) {

        // 可以看到,当初始化一个实例的时候,一次的加载顺序是
        // 静态变量
        // 静态初始化块
        // 普通成员变量
        // 普通初始化块
        // 构造器
        new NoExtendTest();
    }
}

有继承,并且会触发初始化的情况


public class ExtendTest {

    public static void main(String[] args) {
        // 父类静态变量
        // 父类静态初始化块
        // 子类静态变量
        // 子类静态初始化块
        String test = Child.childStaticField;

        // 父类静态变量
        // 父类静态初始化块
        // 子类静态变量
        // 子类静态初始化块
        // 父类变量
        // 父类初始化块
        // 父类构造器
        // 子类变量
        // 子类初始化块
        // 子类构造器
        new Child();

    }
}

class Super {
    public static String parentStaticField = "父类静态变量";

    public static final String gst = "aaa";

    public String parentField = "父类变量";

    static {
        System.out.println( parentStaticField );
        System.out.println( "父类静态初始化块" );
    }


    {
        System.out.println( parentField );
        System.out.println( "父类初始化块" );
    }

    public Super() {
        System.out.println( "父类构造器" );
    }
}

class Child extends Super {
    public static String childStaticField = "子类静态变量";

    public String childField = "子类变量";


    static
    {
        System.out.println( childStaticField );
        System.out.println( "子类静态初始化块" );
    }


    {
        System.out.println( childField );
        System.out.println( "子类初始化块" );
    }

    public Child()
    {
        System.out.println( "子类构造器" );
    }
}

存放在数组中,不触发初始化的情况


public class NoInit {

    public static void main(String[] args) {
        // 不会触发初始化
        Super[] test = new Super[10];

    }
}
class Father {
    public static String parentStaticField = "父类静态变量";
    static
    {
        System.out.println(parentStaticField);
        System.out.println("父类静态初始化块");
    }

    public Father() {
        System.out.println( "父类构造器" );
    }

}

class Son extends Super {
    public static String sonStaticField = "子类静态变量";
    static
    {
        System.out.println(sonStaticField);
        System.out.println("子类静态初始化快");
    }
    public Son() {
        System.out.println( "子类构造器" );
    }
}

访问类中的(static final)常量,不会触发初始化,但是访问类中的 static 的对象是会触发初始化的


public class FinalInit {
    public static void main(String[] args){
        // Final变量
        System.out.println(FinalFieldClass.finalField);
    }
}

class FinalFieldClass {

    public static String staticField = "静态变量";

    static {
        System.out.println("静态初始化块");
    }

    public static final String finalField = "Final变量";
}

上面的代码我上传 github

类的生命周期

生命周期

如上图所示,类的生命周期包含了加载连接(验证,准备,解析),初始化使用卸载阶段,连接阶段包含了验证准备解析,其中类的加载验证准备初始化卸载这五个阶段的顺序是确定的,但是解析阶段不一定,可能会在初始化之后再开始解析,这主要是因为Java语言支持的运行时绑定而导致的

我说一下类的加载连接和初始化阶段所干的事情

1.加载阶段

加载阶段,主要完成了以下三个任务:

  1. 通过一个类的全限制名类获取 .class 的二进制字节流
  2. 将这个字节流转化为存储在方法去的类结构
  3. 在堆内存中生成这个类的 Class(java.lang.Class)对象,作为方法区中类结构的入口

2.连接阶段

  1. 验证:用于确保加载类的正确性,包括文件格式的验证,源数据验证,字节码验证和符号引用验证,验证阶段是很重要,但是并不是必须的,所以可以通过参数-Xverifyone来关闭大部分的验证,可以提高加载的时间
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值,这些内存都在方法区中分配
    • 这个阶段初始化的是类变量,不包含示例变量,实例变量的初始化时和对象初始化一起在堆内存中分配
    • 同事,该阶段初始化的值是数据类型的默认值,而并非代码中制定的值
    • 如果字段是被 static final 所修饰,即我们口中的常量,那么在该阶段会被初始化为 java 代码中指定的值
  3. 解析:解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,解析动作主要是针对类,接口,类方法,接口方法,方法类型,方法句柄和调用限定符7类符号引用进行的,所谓符号引用,就是一组符号来描述目标,可以是任何字面量,直接引用就是直接指向目标指针,相对偏移量或一个间接定位到目标的句柄

3.初始化(这里的初始化才真正使我们上面代码所能体现的)

初始化阶段是执行类构造器 () 方法的过程,() 方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,通过以上手机对类进行初始化,还根据 java 中指定的值对类变量进行初始化

初始化时机:

  • 遇到 newgetStatic(获取一个静态变量,注意不是static final),putStatic(为一个static变量赋值操作)和 invokeStatic(调用一个静态的方法)这4条字节码指令,如果类没有初始化,那么就会对其进行初始化
  • 使用 java.lang.reflect 包中的方法对类进行反射调用的时候,如果类还没有初始化,也会对其进行初始化操作
  • 当初始化一个类时,如果其父类没有初始化,则会县初始化其父类,在初始化其本身
  • 当虚拟机启动时,需要制定一个主类(main方法所在的类),虚拟机会首选初始化这个主类
  • 当使用JDK1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic 、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

注意:

1. 调用类的静态字段,只有直接定义该字段的类才会初始化,如通过子类调用父类的静态变量,只有父类会初始化

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

对象的实例化

对象的实例化过程包含了类的加载过程,分配对象的内存,设置默认值,赋值以及执行构造方法

1.加载类

2.在堆中为对象分配存储空间

3.对象中的字段设置默认值

4.对字段进行赋值操作

5.执行构造方法

以上是我对于类加载的一些理解,如果那里有问题欢迎大家提出来共同探讨一下

同时本文中引用了另一篇文章的对类加载的解释过程 https://my.oschina.net/kun123/blog/1031380