类的加载时机

  最近在学习java虚拟机方面的东西,看的是周志明的《深入理解java虚拟机》,看到类的加载尤其是类的加载时机这一块觉得受益匪浅,遂记录一下。

必须初始化的四种情况

有四种情况类是必须要进行初始化的,对于这四种情况原文描述如下:

但是对于初始化阶段,虚拟机规范则是严格规定了有且只有4种情况必须立即对类进行初始化,而加载、验证、准备自然需要在此之前开始。

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

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

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

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

以上四点我们一一用代码来验证,第一点里面说到了四种初始化的场景,分别是:

①用new关键字实例化对象

②读取类静态字段

③设置类的静态字段

④调用一个类的静态方法

在验证之前需要达成一个共识:虚拟机在初始化类时会执行static语句块中的操作,因此我们可以根据静态语句块中的代码是否执行了来判断类是否加载。为此我创建了一个SubClass类

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SubClass {
    static{
        System.out.println("子类初始化");
    }
    public static int a = 10;

    public static int getA(){
        return a;
    }
}

在main方法中分别执行(每次执行一条)以下四条代码来模拟上面四个场景

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        System.out.println(SubClass.a);
        SubClass.getA();
        SubClass.a = 30;
    }
}

结果不出所料,输出结果都包含"子类初始化",说明以上四种方式确实可以会触发类的初始化。

接下来看第二点,对类进行反射调用时会触发类的初始化

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.test.jvm.classloading.SubClass");
    }
}

以上的反射调用同样正常输出了"子类初始化"。

第三点如果父类没有进行初始化,则要先触发父类的初始化,再创建一个父类,并且让之前的子类继承父类

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SuperClass {
    static {
        System.out.println("父类初始化");
    }
    public static int b = 20;
}
package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SubClass extends SuperClass {
    static{
        System.out.println("子类初始化");
    }
    public static int a = 10;

    public static int getA(){
        return a;
    }
}

这时我们再次执行上面的main方法里面的任意一条测试语句,这时发现在原来的输出"子类初始化"前输出了"父类初始化",说明了两点:①父类同样会初始化;②父类会先于子类初始化。

第四点虚拟机会先初始化包含main方法的主类,这时我们在主类中加入静态代码块

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    static {
        System.out.println("初始化主类");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        SubClass subClass = new SubClass();
    }
}

可以看到输出结果如下,完全印证了第四点。

 类的加载时机

不主动进行初始化 

而对于不会主动进行初始化的情况在该书中也有以下几种情况

第一种是通过子类类名调用父类静态代码(包括静态方法和静态变量)不会进行初始化,以下也通过代码进行说明

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(SubClass.b);
    }
}

输出如下,可以看到只初始化了父类而没有初始化子类。

类的加载时机

 第二种是通过数组来创建对象不会触发此类的初始化

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        SuperClass[] supers = new SuperClass[10];
    }
}

输出为空。

类的加载时机

 第三种是调用final修饰的常量不会触发类的初始化,为此我在父类中加了一个常量

package com.test.jvm.classloading;

/**
 * @author fc
 */
public class SuperClass {
    static {
        System.out.println("父类初始化");
    }
    public static int b = 20;

    public final static String STATE = "常量";
}
package com.test.jvm.classloading;

/**
 * @author fc
 */
public class Main {
    public static void main(String[] args) {
        System.out.println(SuperClass.STATE);
    }
}

可以看到输出结果只是打印了常量的值,并没有初始化这个类。

类的加载时机

补充

 到这里对于书中描述的类的加载时机都已经用例子说明了,接下来展示一个在博主 Boblim那看到的一个例子

/**
 * @author fc
 */
class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon.getInstance();
        System.out.println("count1=" + SingleTon.count1);
        System.out.println("count2=" + SingleTon.count2);
    }
}

输出count1=1,count2=0,关于为什么会输出这个结果在那篇链接的博客已经做了详细的说明,同时这个输出结果也很好地佐证了下面这句话

类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

正是给类变量赋值时是按照顺序进行的,所以上面count2又会被重新赋值为0,才导致这个输出结果。

上一篇:Java类的加载时机


下一篇:C++ 笔记继承机制中的构造器和析构器