虚拟机类加载机制
一、虚拟机类加载机制
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
二、类加载时机
类的生命周期:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
什么时候开始加载?Java虚拟机规范中并没有进行强制约束,虚拟机的具体实现来自由把握。
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字
实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
对于静态字段,只有直接定义这个字段的类才会被初始化。
接口加载过程与类加载过程的区别:
1)接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器 ,用于初始化接口中所定义的成员变量。
2)当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
三、类加载过程
1. 加载
在加载阶段,虚拟机需要完成以下3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。数组类的元素类型最终是要靠类加载器去创建。
数组类创建过程规则:
1)如果数组的组件类型是引用类型,那就递归采用加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识。
2)如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。
3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的。
2. 验证
验证是连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
1)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。(基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,
所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。)
验证点:
是否以魔数0xCAFEBABE开头。
主、次版本号是否在当前虚拟机处理范围之内。
常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
……
2)元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
验证点:
这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
类中的字段、方法是否与父类产生矛盾。
……
3)字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4)符号引用验证:在虚拟机将符号引用转化为直接引用的时候(解析阶段)。
符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
验证点:
符号引用中通过字符串描述的全限定名是否能找到对应的类。
在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
……
2. 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。
这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
3. 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。
如果有了直接引用,那引用的目标必定已经在内存中存在。
4. 初始化
前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程。
以下几种情况不会执行类初始化:
1) 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2) 定义对象数组,不会触发该类的初始化。
3) 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4) 通过类名获取 Class 对象,不会触发类的初始化。
5) 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6) 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作
三、双亲委派模型
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
作用:1. 使用不同的类加载器最终得到的都是同样一个 Object 对象。避免重复加载。
2. 避免核心类篡改。
1. 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2. 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3. 负责加载用户路径(classpath)上的类库。