深入理解java虚拟机(7):类的加载机制
虚拟机把类文件加载到内存,并对数据进行效验,解析和初始化,最终形成java虚拟机可以直接执行的java类型,这就是虚拟机的类加载机制。java在运行期间进行类的加载、连接和初始化,这就造就了java的可以动态扩展。
类的生命周期:
加载,验证,准备,解析,初始化,使用,卸载。初始化的五种情况:1、new,getstatic,putstatic,invokestatic 2、反射调用 3、子类初始化父类跟着初始化 4、虚拟机启动需要一个执行的主类。5、如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个句柄所对应的类没有初始化,就会初始化。被动引用不会触发类的初始化,以下一几个被动引用,
第一个是静态变量的调用
package com.fj.test
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
第二个只是初始化[Lcom.fj.test.SuperClass并不会触发SuperClass的初始化
package com.fj.classloading
public class NotInitialization{
public static void main(String[] args){
SuperClass[] sca = new SuperClass[10];
}
}
第三个是编译器对编译期间将常量存入了NotInitialization类的常量池,不需要初始化
package com.fj.classloading
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)
}
}
当一个类初始化时要求其父类都初始化了,但是接口不是接口使用父接口
一、类加载需要完成以下三件事:
1、通过一个类的全限定名来获取类定义的二进制字节流。
2、将这个字节流代表的静态数据结构转换成方法区运行的数据结构。
3、在内存中生成一个java.lang.Class对象,作为方法区这个类的各种数据访问入口。
在类加载完毕后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区,存储格式可以自行定义。
加载字节码阶段和连接阶段都是交差进行,但是两个的开始时间是有严格先后顺序的
二、验证是连接的第一步,主要有以下几种验证
1、格式验证,主要检查是否以cafebabe魔数开头,版本号是否是虚拟机支持范围内,常量池的引用是否有不支持的类型,指向常量池的索引是否有指向不存在的常量和不支持的类型,CONSTANT_UTF8_info型的常量是否有不符合utf8编码的类型,class文件是否有被删除或者附加的信息,这个阶段主要是保证字节码能正确的存储在方法区,是基于二进制流的验证。
2、元数据验证,是对字节码描述的语义分析,保证符合规范,验证是否有主类,是否有父类,是否是抽象类。
3、字节码验证,主要是通过数据流分析和控制流分析,确定程序语义是合法和符合逻辑的,这个阶段对类的方法体进行分析,保证效验类的方法在运行时不会做出危害虚拟机的行为。
4、符号引用验证,发生在虚拟机将符号引用转换成直接引用的时候,这个转化动作将在连接的第三阶段进行,解析阶段发生,符号引用可以看做是对类外的信息进行效验,主要验证是否能找到类,是否可以访问,
三、准备阶段
准备阶段是对类变量设置分配内存和设置初始值的阶段,类变量不等于初始变量,就是初始变量,实例变量会分配在堆里面,类变量分配在方法区。初始值是数据零值例如
public static int a = 123初始值a=0,123是在类初始化阶段赋值。特殊情况在字段表中有ConstantValue属性,那么准备阶段value会被赋值成为ConstantValue的属性所指的值。例如
public static final a= 123编译期间会为a生成ConstantValue属性,在准备阶段虚拟机就会为a赋值ConstantValue属性。
四、解析阶段
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,
符号引用以一组符号来描述所引用的模板,与虚拟机内存没有直接关系,符号引用的字面量形式明确定义在java虚拟机规范的class文件形式中。
直接引用可以是直接指向目标的指针、偏移量或者句柄,与虚拟机内存直接相关。
1、类和接口的解析
1)如果c不是数组类型,那虚拟机将把代表n的全限定名传递给d的类去加载这个类C
2)如果c是数组类型N的描叙类就是[LXXXX,会按照第一个规则加载xxx类型,然后虚拟机生成一个代表数组维度和元素的数组对象。
2、字段解析
要解析一个未被解析过的符号引用,首先会对字段表内class_index中的索引CONSTANT_Class_info符号进行解析引用,也就是字段所属于的类型和接口的引用,如果解析成功虚拟机将对该类C进行后续的搜索
1》如果C本身包含该字段和字段描述符则返回这个字段的引用,搜索结束
2》如果C实现了接口就会从下往上依次搜索父接口来查找该字段
3》如果C继承了父类则从下往上依次搜索父类查找该字段
实际过程中如果父类和接口同时存在一个字段,编译器会拒绝编译
3、方法解析
类方法解析也需要先解析出类方法表的class_index中的索引方法所属于的类和接口的引用。
1)类方法和接口方法符号引用的常量类型定义分开的,如果在类方法表中发现是接口的引用则抛出java.lang.IncompatibleClassChangeError异常
2)如果通过第一步,在类c中查找是否简单名称和描述一致的方法,如果存在返回
3)在c的父类中查找
4)在c实现的接口以及它的父接口中查找
4、接口解析与类方法解析大致一致
五、类的初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,初始化阶段是执行类构造器<cinit>方法过程。
cinit方法时编译器自动收集类中的所有类变量的赋值动作和静态代码块中语句合并生成,编译器收集的顺序是由语句在源文件中的顺序决定,静态语句只能访问定义在静态语句块前的变量,定义在静态语句块后的变量只能赋值不能访问
public class Test{
static{
i = 0;
System.out.println(i)
}
static int i = 1;
}
cinit与构造方法不同不需要显式的调用父类构造器,虚拟机会保证父类的cinit方法先于子类的cinit方法完成,对于接口或者类不是必要的如果是接口或者类中没有静态代码块或者静态变量的赋值操作。编译器就不需要生成cinit方法。虚拟机会保证多线程先cinit方法的同步操作,如果一个cinit特别耗时间,会导致多个线程阻塞,同一个类加载器,cinit方法只会执行一次