从一个例子看Java的数据初始化和类加载

一、代码镇帖

package javase.jvm;

public class ClassInitTest {

    private static final String staticCodeBlock = " static code block ";
    private static final String codeBlock = " code block ";
    private static final String constructor = " constructor ";

    private static String className = ClassInitTest.class.getSimpleName();

    static {
        //静态初始代码块,用于验证主函数类的加载
        System.out.println(className + staticCodeBlock);
    }

    static class Motherland{

        static final String name = "China";

        static {
            System.out.println("Motherland " + staticCodeBlock);
            //对类的静态变量进行赋值,但是不能使用定义在静态代码块后面的变量
            age = 79;
        }

        //一个静态字段
        static Motherland motherland = new Motherland();

        //静态字段
        static int age = 78;
        static int count;

        {
            //构造代码块
            System.out.println("Motherland " + codeBlock);
        }

        //私有构造器
        private Motherland(){
            System.out.println("Motherland " + constructor);
            age ++;
            count ++;
        }
    }

    static class Successor extends Motherland{

        static String name = "cyf";

        int count1 = getCount2();

        int count2 = 2;

        static {
            System.out.println("Successor " + staticCodeBlock);
            name = "chuyf";
        }

        {
            System.out.println("Successor " + codeBlock);
            count2 = 0;
        }

        Successor(){
            System.out.println("Successor " + constructor);
        }

        int getCount2(){
            return count2;
        }
    }

    public static void main(String[] args){

        System.out.println("Motherland name: " + Motherland.name);

        System.out.println("Successor name: " + Successor.name);

        System.out.println("Motherland age: " + Motherland.age);

        Successor successor = new Successor();

        System.out.println("successor count1: " + successor.count1 + "\t" + "successor count2: " + successor.count2);

        System.out.println("Motherland age: " + Motherland.age);

        System.out.println("Motherland count: " + Motherland.count);
    }
}

二、代码输出

ClassInitTest static code block //主函数类初始化
Motherland name: China //输出静态常量
Motherland  static code block //父类静态代码块
Motherland  code block //父类构造代码块
Motherland  constructor //父类构造函数
Successor  static code block //子类静态代码块
Successor name: chuyf //输出
Motherland age: 78 //输出
Motherland  code block //父类构造代码块
Motherland  constructor //父类构造函数
Successor  code block //子类构造代码块
Successor  constructor //子类构造函数
successor count1: 0	successor count2: 0 //输出对象值
Motherland age: 79 //输出
Motherland count: 2 //输出

三、输出分析

  1. 当虚拟机启动时,虚拟机会先初始化要执行的主类(包含main函数的类)。

第一个输出表示当前主类已经被加载。

  1. 当实例化对象、读取或设置类的静态字段(final修饰的常量除外)、调用类静态方法时将触发类的加载。

第75行输出父类的静态字段,但是并没有引起父类的加载。在编译时,常量传播将该常量直接放置在主类的常量池里面,于是对父类静态字段的引用变成了对自己类常量池的一个引用。使用javap -c -v ClassInitTest.class 命令,可以在Constant pool里面找到这个常量#60 = Utf8 Motherland name: China,所以第75行没有引起其他的类加载(主类已经被加载了)

  1. 当初始化一个类时,如果发现其父类没有进行初始化,则先触发其父类的初始化(所以Object类肯定最先被初始化)。

在第77行访问子类的时候将触发其进行类加载,然后触发其父类的加载,所以输出的为:父类静态代码块(第三行输出) > 子类静态代码块(第六行输出)的大致顺序。之间输出了其他的信息先不管

  1. 虚拟机类加载机制

类的生命周期主要有以下几个阶段:加载、验证、准备、解析、初始化、使用、卸载 七个阶段。除了解析阶段为了支持动态语言可以在初始化后开始,其他的阶段都是顺序开始,交叉进行的。

加载:通过全限定类名获取一个字节流,将其静态存储结构转换为运行时数据结构,并创建一个代表该类的Class对象作为访问该类信息的入口。

验证:确保Class文件里面的字节流不会危害虚拟机本身。包含文件格式验证(是否符合Class文件格式规范、是否被当前虚拟机支持)、元数据验证(字节码描述信息语义分析)、字节码验证(程序语义是否合法、符合逻辑)、符号引用验证(自身信息的匹配验证)。

准备:为类变量分配空间并设置类变量的初始值。常量将被直接赋值为常量值,而不是初始值。

解析:将符号引用解析为直接引用的过程(符号上的逻辑关系转换为虚拟机内存之间的联系)。

初始化:类构造器的执行过程。编译器顺序收集类变量的赋值操作和静态代码块的语句合并而成。

  1. 解释第77行导致的输出

第77行对子类静态字段的读取引发对子类的加载,子类引发对父类的加载。

父类加载过程:

  1. 准备阶段将常量赋值,将静态变量赋值为初始值,准备阶段后各个静态变量的值:name = "China"; motherland = null; age = 0; count = 0;
  2. 初始化阶段收集赋值操作和static代码块对类变量进行初始化。第一个操作,执行静态代码块(第三行输出),对age进行赋值操作,该操作后,各个静态变量的值为:name = "China"; motherland = null; age = 79; count = 0;。第二个操作,初始化motherland,对motherland的初始化需要进行构造,所以需要依次调用构造代码块(第四行输出)和构造函数(第五行输出)。第一步过后,各个变量的值:name = "China"; motherland = [object]; age = 80; count = 1;第三个操作,执行对age的赋值操作,改操作后,各个静态变量的值为:name = "China"; motherland = [object]; age = 78; count = 1;至此父类加载初始化完毕。

子类加载过程:

  1. 准备阶段类变量:name = null;
  2. 初始化阶段,48行将其赋值为:“cyf”,但是在紧随其后的静态代码块(第六行输出)将其修改为:“chuyf”。至此子类初始化完成。
  1. 第79行不会导致类加载,因为都已经被加载过了。
  2. 第81行导致的输出解释

对父类的影响:

  1. 第81行对对象的实例化会调用父类的构造代码块(第九行输出)和构造函数(第十行输出),会将age变为79(第十四行输出),count变为2(第十五行输出)。

对子类的影响:

  1. Java在进行对象创建时:检查是否已经类加载、分配内存、初始化为零值、对象元数据设置、初始化。该类的函数的字节码如下:
    从一个例子看Java的数据初始化和类加载_第1张图片
    反编译的代码如下:
    从一个例子看Java的数据初始化和类加载_第2张图片
    至此,类、实例的实例化顺序复习完成。

四、初始化顺序总结

  1. 父类类构造函数(顺序的静态字段赋值语句与static代码块)
  2. 子类类构造函数(顺序的静态字段赋值语句与static代码块)
  3. 父类的构造代码块和构造函数
  4. 子类类的构造代码块和构造函数

注:

  1. 在进行初始化时,常量在准备阶段就已经赋值,而类静态变量为零值。
  2. 当执行顺序前的初始化操作去使用后续未初始化的值时将会访问到零值(准备阶段),常量除外。
  3. 常量在编译阶段的传播优化不会导致该常量声明类的加载。
  4. 类构造是线程安全的,所以注意类构造的时间。
  5. 最好的方法是看字节码文件,实例变量赋值、构造代码块和构造函数被整合到函数,静态代码块和静态变量赋值被整合到函数。

你可能感兴趣的:(从一个例子看Java的数据初始化和类加载)