相关文章:
虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这一过程称之为类加载机制
在Java中,类型的加载、连接和初始化过程都是在程序运行期间完成的,依赖于运行期动态加载和动态连接这两个特点,实现了 Java 可以动态扩展的语言特性
一、类的生命周期
-
类的生命周期如上所示,其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一样:它在某种情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定 (也称动态绑定或晚期绑定)
-
这五个阶段是按部就班地 “开始”,而不是按部就班地 “进行” 或 “完成”,这是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段
二、必须立即对类进行初始化的五种情况
-
什么时候进行类加载过程的第一个阶段:加载?Java虚拟机规范中并没有强制约束,这点可以交给虚拟机的具体实现来自由把握。但对于初始化阶段,虚拟机规范则严格要求了有且只有五种情况下必须立即对类进行初始化,在此之前先开始类的加载、验证和准备工作
-
遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,生成这四条指令最常见的场景如下所示
指令 作用 new 使用 new 关键字实例化对象 getstatic 读取一个类的静态字段 putstatic 设置一个类的静态字段 invokestatic 调用一个类的静态方法 -
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
-
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
-
当虚拟机启动时,需要指定一个要执行的主类 (包含 main() 方法的那个类),虚拟机会先初始化这个主类
-
当使用 JDK7.0 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄时,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
-
-
这五种场景中的行为称为对一个类进行主动引用,除此之外,所有引用类的方法都不会触发初始化,称为被动引用
-
通过下面的例子,我们来看看什么是被动引用
-
例子一
public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public 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! // 123
-
运行上述代码之后只会输出 “SuperClass init!”,而不会输出 “SubClass init!”,对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
-
如果再使用 final 修饰符对该静态字段进行修饰 (public static final int value = 123),则会出现与例子三一样的情况
-
-
例子二
public class NotInitialization { public static void main(String[] args) { SuperClass[] sca = new SuperClass[10]; } }
-
运行上述代码之后,并没有输出 “SuperClass init!”,这说明并没有触发 SuperClass 的初始化阶段,但是这段代码里触发了另一个名为 “[SuperClass” 的类的初始化,它是一个由虚拟机自动生成的、直接继承于 Object 的子类,创建动作由字节码指令 newarray 触发
-
这个类代表了一个元素类型为 SuperClass 的一维数组,数组中应有的属性和方法都实现在这个类里,但我们可以直接使用的只有被 public 修饰的 length 属性和 clone() 方法
-
-
例子三
public class ConstClass { static { System.out.println("ConstClass init!"); } public static final String HELLOWORLD = "hello world"; } public class NotInitialization { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } }
-
运行上述代码之后,并没有输出 “ConstClass init!”,这是因为虽然在 Java 源码中引用了 ConstClass 类中的常量 HELLOWORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值 “hello world” 存储到了 NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLOWORLD 的引用实际都被转换为 NotInitialization 类对自身常量池的引用
-
也就是说,实际上 NotInitialization 的 Class 文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就不存在任何联系了
-
如果将修饰该常量的修饰符 final 去掉 (public static String HELLOWORLD = “hello world”),则会出现与例子一一样的情况
-
-
三、接口的加载过程
-
接口的加载过程与类加载过程稍有不同,需要做一些特殊说明:
-
接口中不能使用 “static{}” 语句块,但编译器仍然会为接口生成 “<clinit>()” 类构造器,用于初始化接口中所定义的成员变量
-
一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到了父接口的时候 (如引用父接口中定义的常量) 才会初始化
-
四、归纳总结
-
类加载机制
- 将描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 文件
-
动态扩展
- 依赖于运行期动态加载和动态连接这两个特点,实现了 Java 可以动态扩展的语言特性
-
类的生命周期
-
加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载
-
其中验证、准备、解析三个阶段统称为连接阶段
-
-
主动引用 (触发初始化)
-
使用 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则会触发其初始化
-
对类进行反射调用时,如果类没有进行过初始化,则会触发其初始化
-
当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
-
当虚拟机启动时,需要指定一个要执行的主类 (包含 main() 方法的那个类),虚拟机会先初始化这个类
-
当使用 JDK7.0 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄时,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
-
-
被动引用 (不触发初始化)
-
通过子类引用父类的静态字段,不会触发子类的初始化
-
通过数组定义来引用类,不会触发此类的初始化
-
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
-
-
接口初始化
- 一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到了父接口的时候 (如引用父接口中定义的常量) 才会初始化