过完整本深入理解Java虚拟机后,发现对类的初始化加载仍然是一知半解的。记得昨晚在极客时间上看到刘超老师在Java性能调优21章写的Demo时,对Demo的输出顺序感到有些疑惑。便在IDE上进行了测试。
父类:
public class Parent {
public static String parentStr = "parent static String";
static{
System.out.println("parent static fileds");
System.out.println(parentStr);
}
public Parent(){
System.out.println("parent instance initialization");
}
}
子类:
public class Sub extends Parent{
public static String subStr = "sub static String";
static{
System.out.println("sub static fileds");
System.out.println(subStr);
}
public Sub(){
System.out.println("sub instance initialization");
}
public static void main(String[] args){
// System.out.println("sub main");
// new Sub();
}
}
因为看到输出是先打印出父类静态块->子类静态块->sub main()->父类构造函数->子类构造函数。
原本我的猜想是:sub main是在new显示初始化后输出的,但看结果并不是这样,所以猜想应该是在Sub类中的main函数先触发了虚拟机对Sub类的初始化。并且虚拟机发现Sub类的父类仍未初始化;
所以,先对Parent类进行了初始化,按静态块在程序中的位置,顺序输出Parent静态块的内容。
再对Sub类进行初始化,同样安装静态块在程序中的位置,顺序输出Sub块的内容。
而后System.out.println(“sub main”);再进行了打印输出。
执行到new一个子类Sub()时,先执行父类的构造方法,再执行子类的构造方法。所以产生了如上的输出。
其实这个在周志明老师写的《深入理解Java虚拟机》中有过详细介绍:虚拟机规范中规定了有且只有5中初始化方式(P210页-P211页),其中的3、4两种情况。
回看的时候又发现了一些新的东西:
1.被动引动
- 对于静态字段,只有直接定义了这个字段的类才会被初始化。也就是说:通过子类直接调用父类的静态字段,是不会触发子类的初始化。但虚拟机规范未明确规定该动作是否会触发子类的加载和验证,因此不同的虚拟机可能会有不同的实现。其中HotSpot虚拟机中,该操作会导致子类的加载。经在IDEA上JDK8的环境下验证后,确实有此表现,同时猜想我电脑上的JVM默认采用的是HotSpot虚拟机~代码比较简单,就不贴上来占地方啦,结果如图。其中InvokeStatic是父类,其中有静态块,静态方法,InvokeSubStatic继承了InvokeStatic,里面有一个静态块。另有Parent,在main函数中通过子类调用了父类的静态方法。可以看出:子类的静态块并没有输出,但确实有加载子类。要看到这个结果需要配置JVM参数:-XX:+TraceClassLoading。结果如图:
- 数组定义引用类,或者说是构建该类的数组,不会触发该类的初始化。
- 常量(static final)在编译阶段存入**调用类(仅调用被调用类的常量)**的常量池中,在编译阶段通过常量传播的优化,将常量的值传入到了调用类的常量池中,调用类对常量的引用,实际都转化成了调用类对自身常量池的引用。因此其实在编译后,调用类和被调用类就不存在任何联系了。
2.接口的加载过程
接口也有初始化过程,和类是一致的,编译器会为接口生成()类构造器,用于初始化接口中定义的成员变量;接口仅仅在真正使用到的时候(如引用接口中定义的常量)才进行初始化,并不会像类加载一样,要求其父类全被初始化。
当初看的时候懵懵懂懂的,回看几遍并动手时间后才大概了解了这些文字的大体意思。收获颇丰。