类的加载时机
类从加载到卸载一共经历7个步骤:
加载--------验证---------准备----------解析---------初始化----------使用-------------卸载
其中验证、准备、解析又叫做连接的过程
加载、验证、准备、初始化、卸载这五个步骤顺序是固定的,而解析阶段不一定,解析可以发生在初始化之后,为了支持java语言的运行时绑定。
那么什么时候会触发JVM是开始加载一个类的过程呢?JVM规范中并没有强制约束,但是规范严格规定了有且只有5种情况发生时,必须对类进行初始化,那么在这之之前,加载,验证,准备的过程肯定也要完成
- 遇到new、getStatic、putStatic或invokestatic这4条指令字节码时,如果类还没有初始化,则要对类进行初始化操作。生成这4条字节码指令操作:new关键字实例化对象的时候,访问或者设置类的静态变量(被final修饰、已在编译期把结果放入到常量池的的静态字段除外),以及调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候,先对类进行初始化
- 当初始化一个类时,如果他的父类没有被初始化,则首先初始化父类
- 当虚拟机启动时,先会初始化包含main方法的那个类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokestatic的方法的句柄时,先初始化句柄对应的类
这五种行为叫做对类的主动引用,除此之外,所有引用类的方法都不会触发初始化,称为被动引用。
被动引用一
package xidian.lili.classloading;
public class Demo01 {
public static class SupClass{
public static int a=7;
static{
System.out.println("Supclass init");
}
}
public static class SubClass extends SupClass{
public static int b=7;
static{
System.out.println("Subclass init");
}
}
public static void main(String [] args){
//System.out.println(SubClass.b);
System.out.println(SubClass.a);
}
}
通过子类调用父类的静态变量,只会初始化父类。也就是操作哪个类的静态变量就会初始化那个类
被动引用二
package xidian.lili.classloading;
public class Demo2 {
public static class SupClass{
public static final int a=7;
static{
System.out.println("Supclass init");
}
}
public static class SubClass extends SupClass{
public static int b=7;
static{
System.out.println("Subclass init");
}
}
public static void main(String [] args){
//System.out.println(SubClass.b);
System.out.println(SubClass.a);
}
}
继续上一个示例,如果变量a是final修饰的静态变量,那么a在编译期间就被加载到方法区的常量池,就相当于跟类是没有关系的,再调用就是类本身对自身常量池的引用,而不需要通过类的class文件中的符号引用来调用,所以不需要初始化类
被动引用三
package xidian.lili.classloading;
public class Demo2 {
public static class SupClass{
public static final int a=7;
static{
System.out.println("Supclass init");
}
}
public static class SubClass extends SupClass{
public static int b=7;
static{
System.out.println("Subclass init");
}
}
public static void main(String [] args){
//System.out.println(SubClass.b);
//System.out.println(SubClass.a);
SubClass [] subs=new SubClass[10];
}
}
创建一个类的对象数组,不会引发类的初始化创建动作由newarray触发,它触发的是"[Lorg.xidian.lili.classloading.SubClass"类的初始化,是虚拟机自动生成的,直接继承于java.lang.Object的子类。
对于接口的加载
对于接口的加载和类有一些区别,接口也需要初始化,但是接口中不会存在static{}这样的语句块,但编译器还是为接口生成<clinit>()类构造器,用于初始化接口中定义的成员变量。
上述类的5中触发类初始化接口与类有区别的在第三条,就是一个接口在初始化的时候并不要求父接口全部初始化,只有在用到父接口的时候才会初始化。
在初始化一个类时,并不会先初始化它所实现的接口。
在初始化一个接口时,并不会先初始化它的父接口
类加载过程
-
加载
加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。
在加载阶段,JVM需要完成3件事:
1)通过类的全限定名获取该类的二进制字节流;
如果是数组类,前面我们说过数组类,它触发的是"[Lorg.xidian.lili.classloading.SubClass"类的初始化,是虚拟机自动生成的,直接继承于java.lang.Object的子类,所以如果数组类的创建就要遵循以下:
如果数组类的元素是引用类型,那就递归采用正常的加载过程去加载元素,数组类将在元素类型的类加载器的类名称空间上被标识
如果数组的元素是基本数据类型,比如 int[] a=new int [10],JVM会把数组类与引导类加载器关联
2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-
验证
确保class文件的字节流符合当前虚拟机的要求
1)文件格式验证
验证class文件的魔数,主次版本号,常量池是否有不被支持的常量类型等等。只有经过这步验证,字节流才会进入到方法区存储,后面的验证不在接触二进制流,而是基于它在方法区的存储结构进行的
2)元数据验证
对字节码信息进行语义分析
比如这个类是否有父类,是否继承了不被允许继承的类,final修饰,是否实现了继承的抽象类中的所有方法。保证元数据信息都符合java语言规范
3)字节码验证
元数据验证对数据类型做完校验,这个阶段是对方法体进行校验分析
4)符号引用验证
这个阶段发生在解析阶段,确保解析阶段可以正常执行,如果这个阶段验证失败,抛出
java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError等异常。可以验证;
是否可以通过类的全限定名找到类
在指定类中是否存在符合方法描述的字段信息以及简单名称描述的方法和字段
符号引用的类,字段,方法的访问权限是否可以被当前类访问
如果运行的代码都已经被反复用过了,就可以在实施阶段使用参数-Xverify:none来关闭参数验证
是为类变量分配内存并设置初始零值的,类变量将在方法区分配,设置初零值根据类型的不同设置不同的零值(boolean类型的默认值是false)。但是如果类变量是被final修饰,那么准备阶段就会直接根据预设的值赋值给变量
-
准备阶段
-
解析
解析就是把符号引用替换为直接引用的过程。
符号引用:是一组符号来描述所引用的对象,可以是任意形式的自面量,与虚拟机实现的内存布局无关
直接引用:可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用与虚拟机的内内存布局相关,有了符号引用那么目标必定已经在内存中存在了
前面说过解析不一定会发生在什么时候,虚拟机根据需要来判断到底是在类被加载的时候就对常量池中的符号进行解析还是等到一个符号引用要被使用前去解析它。
-
初始化
初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。前面讲到了类的主动引用 和 被动引用就是在初始化的时候发生
完成初始化在JVM中就完成了类加载的过程