上一篇博客中(讲解面试题那一篇)只是从运行结果的表象来分析了java中类的加载时机,近两天走马观花的读了周志明老师的《深入理解java虚拟机》一书,再来回答java类的加载时机。下面的回答大部分会引用原书,加少量自己的实践。
在周老师的书中,虚拟机类的加载机制为一大章,然后分分为三小节具体深入讲解①.类的加载时机②.类的加载过程(加载.验证.准备.解析.初始化)③.类加载器(类与类加载器.双亲委派模型.破坏双亲委派模型)。这种经典书籍希望大家也可以买来细细品读。这次我们只说类的加载时机。
原书中这样说:
①.遇到new.getstatic.putstatic或invokestatic这四条字节指令码时,如果没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字初始化对象的时候、读出或设置一个类的静态字段_(被final修饰、已经在编译时期把结果放入常量池的静态字段除外)_的时候,调用一个类的静态方法的时候。
②.使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有进行过初始化,则需先触发其初始化。
③.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
④.当虚拟机启动时,用户还需要指定一个需要执行的主类(包含main()方法的那个类),虚拟机会先去初始化这个主类。
⑤.当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有进行过初始化,则需先触发其初始化。
对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外引用类的方式都不会触发初始化,称为被动引用。下面举三个例子来说明何为被动引用,分别说明:
代码1:
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
上述代码执行后,只会输出“SuperClass init!”,而不会输出“SubClass Init!”。对于静态字段,只有直接定义这个字段的类才会别初始化,因此通过其子类来引用父类中的静态字段,只会触发父类的初始化而不会触发子类的初始化。
代码2:
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SubClass[10];
}
}
上述代码执行后并没有输出“SuperClass init!”,说明并没有触发SuperClass的初始化阶段。
代码3:
上述代码执行后,也没有输出“ConstClass init!”,这是因为虽然在java源码中引用了ConstClass类中的常量HEELLOWORID,但其实在编译阶段通过常量传播优化,已经将其常量的值“Hello world” 存储到了 NotInitialization类的常量池中,以后NotInitialization对常量的ConstClass。HELLOWORLD的引用世界都转化为NotInitialization类对自身常量池的引用了。也就是说,实际上NotInitialization的Class文件之中zaimeiyouConstClass类的符号引入口,这两个类在编译成Class之后就不在有任何联系了。
上面三个代码来看,以前对类的加载时机误解还是真不少,比如类的加载与类的初始化根本就是两回事,读者可以在前两个代码的主函数中加入如下代码来查看java虚拟机中已经加载进来的类。
Field f=ClassLoader.class.getDeclaredField("classes");
f.setAccessible(true);
Vector classes=(Vector)f.get(ClassLoader.getSystemClassLoader());
System.out.println(classes);
前两个代码,用到的三个类都加载进了内存,只是部分类没有初始化,最后一个代码只有含有主函数的类加载进了内存。初始化,粗劣的可以理解为:对加载进来的类可以执行,变量的赋值和静态代码块等的执行。(具体请参看原书)
也就是说只要记住周老师书中所说的那五种有且仅有的原因,其他的情况都不会对类进行初始化。
关于周老师书中说的五点,我来细化一点,关于第二条。
第二条:看如下代码:
class Dog {
static {
System.out.println("Dog init!");
}
}
public class ReflectInitializtion {
public static void main(String[] args) throws Exception {
Class.forName("com.test.Dog");
}
}
此时会把Dog类加载进内存,并进行初始化。
修改代码后
class Dog {
static {
System.out.println("Dog init!");
}
}
public class ReflectInitializtion {
public static void main(String[] args) throws Exception {
Class clazz = Dog.class;
}
}