虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析,初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java的类加载机制。
类的加载,连接,初始化是在程序运行时完成的。
生命周期
加载—->连接(验证->准备->解析)—->初始化—->使用—->卸载
加载,验证,准备,初始化和卸载这5个步骤是确定的,类加载过程必须按照这个顺序。而解析阶段则不一定,某些情况下可以在初始化之后再开始,这是为了支持Java语言的动态绑定。
初始化的条件
Java虚拟机规范规定遇到一下5种情况必须立即对类进行初始化。
- 遇到new,getstatic,putstatic或者invokestatic这4条字符码指令时,如果类没有进行过初始化,则必须先触发其初始化。生成这4条指令常见的Java代码有:使用new关键字实例化对象的时候,读取或设置一个类的静态变量(被final修饰的在编译期把结果放在常量池的静态字段除外),以及调用一个类的静态方法时。
- 使用reflect包的方法对类进行反射调用时
- 当初始化一个类的时候发现其父类还没有初始化时,则需先触发其父类初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的解析结果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄时,并且这个方法的类没有进行过初始化。
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
加载
在加载的阶段,虚拟机完成以下3件事
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.class对象(并没有明确规定在Java堆中,对于HotSpot虚拟机来说,Class对象比较特殊,存放在方法区)
验证
验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息字符符合虚拟机的要求,并且不会危害虚拟机的安全。
如果输入的字节流不符合虚拟机的规范,虚拟机会抛出java.lang.VerifyError异常或子类
验证内容包括
- 文件格式验证
是否以魔数0xCAFEBABE开头
主,次版本号是否在当前虚拟机处理的范围内
常量池的常量是否有不被支持的常量类型
CONSTANT_Utf8_info型的常量 中是否有不符合UTF8编码的数据
Class文件中各部分及文件本身是否被删除或者附加其它信息 - 元数据验证
对字节码描述的信息进行了语义分析,包括
这个类是否有父类(除了object类型,其余都有)
这个类的父类是否继承了不允许被继承的类(final)
类的字段,方法是否与父类产生矛盾(覆盖final字段,不合规的重载) - 字节码验证
主要目的是通过数据流和控制流分析确定程序语义是合法的,复合逻辑的。
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,不会出现操作栈放置了一个int型,但是却按照long类型加载到本地变量表;
保证跳转 指令不会跳转到方法体以外的字节码指令上;
保证方法体中的类型转换有效(例如把一个子类对象赋值给父类是安全的,但是反过来是危险的) - 符号引用验证
最后一阶段,虚拟机将符号引用转化为直接引用,转化的动作在解析的阶段发生。符号引用校验可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常包括一下内容
符号引用通过字符串描述的全限定名能否找到对应的类;
指定的类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;
符号引用中的类,方法,属性的访问性是否能被当前类访问
验证阶段非重要但是不是一定必要,确保代码是可靠的,可以通过-Xverify:none参数关闭验证
准备
正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都会在方法区进行分配(该阶段进行内存分配的仅仅包括static变量,不包括实例变量)。其次这里所说的初始化“通常情况”下是数据类型的零值。
非通常情况:如果类字段的字段属性表中存在ConstantValue的属性,则在准备阶段初始化为ConstantValue属性所指定的值。如final static定义的变量
解析
解析阶段是将常量池内的符号引用替换成直接引用的过程
符号引用: 符号引用是以一组符号来描述所引用的目标,可以是任何形式的字面量(CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info),与虚拟机内存布局无关,引用的目标也不一样加载到内存中。
直接引用:可以是直接指向目标的指针,相对偏移量或者一个间接定位目标的句柄,与虚拟机的内存布局有关。
虚拟机并未规定解析阶段的具体时间,只要求在执行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield和putstatic这16个用于操作符号引用的字节码指令之前,对他们使用的符号引用进行解析。
除了invokedynamic(为了支持动态语言的指令,程序必须运行到这条指定时,解析动作才能进行)指令外,一个符号引用只进行一次解析(在运行时的常量池中记录直接引用,并把常量标志为已解析)
解析的类型
类
接口
字段
类方法
接口方法
方法类型
方法句柄
调用点限定符
类或接口的解析
假设代码所处的类为D,如果要把未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机需要完成3个步骤
- 如果C不是数组类型,虚拟机将C的全限定名传递给D的类加载器去加载这个类C(递归),一旦加载过程异常,解析过程失败
- 如果C是数组类型,并且数据的元素类型为对象,也就是N的描述符为[Ljava/lang/Integer,那将按照第1点的规则加载数组元素类型。如果N的描述符如前假设,则加载的元素类型为java.lang.Integer,接着虚拟机生成一个代表此数组维度和元素的数组对象
- 如果上述步骤正常,C在虚拟机中已经成为了有限的类或接口了,但在解析之前要进行符号引用验证,确认D具备C的访问权限。
字段的解析
解析字段的符号引用之前,先要对字段表内的Class_index中索引的类或接口的符号引用进行解析
将这个字段所属的类或接口用C表示
- 如果C本身包含的简单名称和字段描述符与目标匹配,则返回
- 否则,如果C实现了接口,则按照继承关系从下往上递归搜索各个接口
- 否则,查找失败
类方法解析
第一步也是先解析方法所在的类,类方法和接口方法是分开的
- 查找是否有简单名称和描述符与目标复合的
- 否则,在父类中递归查找
- 接口,抽象类之中查找,找到抛出异常
接口方法解析
同类方法的接口,只不过是在接口中匹配
初始化
在准备阶段,变量以及赋过系统要求的初始化。而在初始化阶段,则根据程序员的主观设计去初始化类变量和其它资源。
初始化即是执行类构造器< clinit>()方法的过程。
< clinit>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译收集的顺序由语句在源文件中出现的顺序决定。
静态语句块只能访问定义在静态语句块之前的变量,定义在之后的可赋值但是不能访问
虚拟机在调用< clinit>()方法之前保证父类的< clinit>()方法已经执行完毕。虚拟机保证一个类的< clinit>()方法在多线程环境中被正确地加锁,同步。
类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
双亲委派模型
Bootstrap ClassLoader启动器加载器,加载\lib目录下的jar;
Extension ClassLoader扩展类加载器,加载\lib\ext目录下的jar;
Application ClassLoader应用程序类加载器,加载用户路径下的类库