类从被加载到JVM
中开始,到卸载出内存为止,它的这个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中加载、验证、准备、解析、初始化是类加载的过程。本章内容是类的初始化阶段。
初始化时机
- 遇到
new、getstatic、putstatic、invokestatic
这4条字节码指令时,如果类没有进行过初始化,要进行初始化。其中final
修饰的静态字段除外,因为final static
修饰的字段在编译期已经把结果放入常量池 - 使用
java.lang.reflect
包的方法对类进行反射调用的时候 - 子类初始化时发现其父类还没有初始化,要先触发其父类初始化
JVM
启动时,用户需要指定包含main
方法的类,JVM
会先初始化这个类- 使用
JDK1.7+
的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,并且这个方法句柄对应的类没有进行过初始化时,要先触发其初始化。MethodHandle
与Reflection
类似,都是模拟方法调用,但是MethodHandle
是模拟字节码层次的方法调用,并且只是包含方法调用相关信息,Method
中是包含了签名、描述符等方面的信息。
对于上述5中场景,JVM规范使用有且只有的限定于来表述,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
被动引用实例
- 通过子类(注意不是对象)引用父类的静态字段,不会导致子类初始化,但是父类会初始化
- 通过定义类的数组,不会触发此类初始化
- 常量在编译阶段会存入调用类(注意是调用类)的常量池,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类初始化
类的初始化过程
初始化是类加载过程的最后一步,这个阶段是根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,从java
字节码的角度表达:初始化阶段是执行类构造器<clinit>()
方法的过程。
<clinit>()
方法是由编译器自动收集类中的所有 类变量的赋值动作和静态代码块(static{}
) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态代码块中只能访问到定义在静态代码块之前的变量,定义在它后面的变量,在静态代码块中只能赋值,而不能访问。
<clinit>()
方法与类的构造函数(<init>()
方法)不同,JVM
会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕,因此父类的静态代码块会优先子类的静态代码块执行。在JVM
中,java.lang.Object
是第一个被执行的类。
<clinit>()
方法对于类或者接口不是必需的,如果一个类没有静态代码块,也没有对类变量的赋值操作,编译器可以不为这个类生成此方法。
接口中没有静态代码块,但有变量初始化的赋值操作,因此也会生成<clinit>()
方法,但是接口和实现类不需要先执行父接口的<clinit>()
方法,只有在使用接口中定义的变量时,父接口才会初始化。
虚拟机会保证<clinit>()
方法在多线程环境中被正确的加锁、同步,如果多线程同时初始化一个类,那么有且只有一个线程去执行这个类的<clinit>()
方法。
接口的初始化过程
接口的加载过程与类加载过程稍有不同,针对接口的特殊说明:接口中不能使用static{}
代码块,但编译器仍然会为接口生成clinit
类构造器,用于初始化接口中定义的成员变量。接口在初始化时,不要求其父接口全部都完成了初始化,只有在真正使用到父接口时(如引用接口中定义的常量)才会初始化。