由于道行不够深,所以此篇类加载机制的讲解主要来自于《深入理解Java虚拟机——JVM高级特性与最佳实践》的第7章 虚拟机类加载机制。
在前面《初识Java反射》中我们在开头提到要了解Java反射,就得要了解虚拟机的类加载机制。在这里,我们来试着窥探一下何为类加载。
“虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,类型的加载、连接和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。”这句话确实读着好懂,但到底类加载做了什么呢?我们都知道Java编译后形成.class字节码文件,虚拟机是不认识.java文件的,所以虚拟机要加载Class文件将它做一些处理才能到“还原”成我们所写的java程序,按照我们的逻辑步骤来执行。Java之所以称为动态语言正是因为类型的加载、连接和初始化都是在程序运行期间完成的。这虽然会带来一些开销,但同时它也为Java语言带来了很大的灵活性。
那么在此期间虚拟机做了什么呢?我们可以通过下面的图来了解类的整个生命周期:加载、验证、准备、解析、初始化、使用、卸载。
这7个阶段中的:加载、验证、准备、初始化、卸载的顺序是固定的。但它们并不一定是严格同步串行执行,它们之间可能会有交叉,但总是以“开始”的顺序总是按部就班的。至于解析则有可能在初始化之后才开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
对于加载阶段(注意加载和类加载概念,加载是类加载过程的第一个阶段)JVM并没有对此约束,而是交由虚拟机的具体实现。但对于初始化,虚拟机规范则做了严格的规定,初始化可能也是对我们实际编程运用当中非常值得注意的问题。
虚拟机对于类的初始化阶段严格规定了有且仅有只有5种情况如果对类没有进行过初始化,则必须对类进行“初始化”!
- 遇到new、读取一个类的静态字段(getstatic)、设置一个类的静态字段(putstatic)、调用一个类的静态方法(invokestatic)。
- 使用java.lang.reflect包的方法对类进行反射调用时。
- 当类初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(如果是接口,则不必触发其父类初始化)
- 当虚拟机执行一个main方法时,会首先初始化main所在的这个主类。
- 当只用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(暂未研究此种场景)
上面5种场景是有且仅有,称之为“主动引用”,只有满足上述5种场景之一,就会触发对类进行初始化。其余都不会触发类初始化,称之为“被动引用”。
下面列举3个例子来说明何为“被动引用”:
被动引用——例1
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class SuperClass { 9 static{ 10 System.out.println("SuperClass init!"); 11 } 12 public static int value = 123; 13 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class SubClass extends SuperClass { 9 static{ 10 System.out.println("SubClass init!"); 11 } 12 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 System.out.println(SubClass.value); 15 } 16 17 }
输出结果:
我们看到虽然我们是通过子类来调用的父类静态字段,但从结果可以看到并没有初始化子类,而是初始化了父类,这即是“被动引用”。对于静态字段,只有直接定义这个字段的类才会被初始化,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
被动引用——例2
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 SuperClass[] sca = new SuperClass[10]; 15 } 16 17 }
我们还是利用例1的SuperClass来创建一个数组,这个应该都知道,在创建数组时并不会初始化,在编程不注意的时候可能常常因为没有初始化数组导致空指针的情况。它仅仅做了创建一个大小为10的数组。
被动引用——例3
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class ConstClass { 9 static { 10 System.out.println("ConstClass init!"); 11 } 12 13 public static final String HELLO = "hello"; 14 }
1 package day_13_passiveReference; 2 3 /** 4 * @author turbo 5 * 6 * 2016年9月19日 7 */ 8 public class Main { 9 10 /** 11 * @param args 12 */ 13 public static void main(String[] args) { 14 System.out.println(ConstClass.HELLO); 15 } 16 17 }
这个例子的输出会初始化ConstClass类吗?
答案是并不会。这是因为常量在编译阶段会存入调用类的常量池中,本质上并没有直接饮用到定义常量的类。进一步解释,虽然在main方法中引用了ConstClass类中的常量HELLO,但其实在编译阶段通过常量传播优化,已经将此常量的值“hello”存储到了Main类的常量池中,之后对ConstClass.HELLO的引用实际上都被转化为Main类对自身常量池的引用。也就是说,两个类在编译过后实际上不存在任何联系了。
类加载时机就讲到这里了,类加载的初始化步骤非常重要的一个步骤,理解清楚“初始化”对我们写出高质量不易出错的代码非常重要。