前言
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.加载阶段
加载阶段,主要完成了以下三个任务:
- 通过一个类的全限制名类获取 .class的二进制字节流
- 将这个字节流转化为存储在方法去的类结构
- 在堆内存中生成这个类的Class(java.lang.Class)对象,作为方法区中类结构的入口
2.连接阶段
- 验证:用于确保加载类的正确性,包括文件格式的验证,源数据验证,字节码验证和符号引用验证,验证阶段是很重要,但是并不是必须的,所以可以通过参数-Xverifyone来关闭大部分的验证,可以提高加载的时间
- 准备:为类的静态变量分配内存,并将其初始化为默认值,这些内存都在方法区中分配
    - 这个阶段初始化的是类变量,不包含示例变量,实例变量的初始化时和对象初始化一起在堆内存中分配
- 同事,该阶段初始化的值是数据类型的默认值,而并非代码中制定的值
- 如果字段是被 static final所修饰,即我们口中的常量,那么在该阶段会被初始化为 java 代码中指定的值
 
- 解析:解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,解析动作主要是针对类,接口,类方法,接口方法,方法类型,方法句柄和调用限定符7类符号引用进行的,所谓符号引用,就是一组符号来描述目标,可以是任何字面量,直接引用就是直接指向目标指针,相对偏移量或一个间接定位到目标的句柄
3.初始化(这里的初始化才真正使我们上面代码所能体现的)
初始化阶段是执行类构造器 
初始化时机:
- 遇到 new,getStatic(获取一个静态变量,注意不是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
