jvm类加载机制
jvm虚拟机将描述类信息的class文件加载到内存中,并对数据进行校验、转换、解析和初始化,最终将形成可以被jvm虚拟机直接使用的java类型,这就是jvm的类加载机制。
在了解加载机制前,我们先了解下类加载器。
一、类加载器
jvm中有两种类型的类加载器,分别是由C++编写和由java编写的类加载器。下面分别介绍类加载器
- BootStrap ClassLoader: 启动类加载器,由C++编写,加载jre/lib/rt.jar包下的子包,例如:java.util包下,java.lang包下
- Extension ClassLoader: 扩展类加载器,由java编写,加载jre/lib/ext/*.jar包下的所有子包,
- Application ClassLoader: 应用程序类加载器,由java编写,加载项目class_path目录下的所有jar,也就是加载系统上下文的所有类
- 自定义类加载器 :加载我们自定义目录下的class文件
然而这些类加载器之间有着一种联系,自定义类加载器 ——>父类为——>Application ClassLoader——>父类为——->Extension ClassLoader———> 父类为——->BootStrap ClassLoader
二、双亲委派机制
如果一个类加载器收到了加载某个类的请求后,而这个类加载器并不会加载这个类,而是交给自己的父类来加载这个类,如果这个父类上面还有父类的话,还会交给自己的父类来加载,只有当父类加载器在其搜索范围内无法找到所需的类,并将结果返回给子类加载器,子类加载器才会尝试自己加载,这种想象叫做双亲委派机制
三、类加载
下面了解类加载过程:
加载 ——-> 连接(验证、准备、解析) ——-> 初始化 ——-> 使用 ——-> 卸载
类从被加载到内存中开始,到卸载出内存,经历了加载、连接、初始化、使用四个阶段,其中连接又包含了验证、准备、解析三个步骤。
这些步骤总体上来说是按照上面的顺序来执行,但是java语言支持运行时绑定,所以解析阶段也可能在初始化之后进行,以上顺序都只是说开始的顺序,实际过程是交叉执行的。
1.类加载时机
首先需要知道什么时候类需要被加载,java虚拟机规范没有约束这一点,但是却规定了必须进行初始化的五种情况
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令
- 调用了反射
- 初始化一个子类会加载其父类
- 虚拟机启动时,用户指定的主类(main函数所在类)
- 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化
其中,情况1中的四条字节码指令在java中常见的场景:
- new 一个对象
- set或者get一个类的静态字段(除去被final修饰的放入常量池中的静态字段)
- 调用一个类的静态方法
2.类加载过程
加载:加载一般分为以下几步
- 通过一个类的全限定名获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证:验证作为连接的第一步,用于确保类或接口的二进制结构上是正确的,从而字节流包含的信息对于虚拟机来说是安全的。大体上是完成下面几个验证
文件格式验证 :主要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理。主要验证,
- 是否以魔数
0xCAFEBABE开头 - 主次版本号是否在当前虚拟机处理范围之内
- 常量池的常量是否有不被支持的类型 (检查常量tag标志)等等
- 是否以魔数
元数据验证: 主要对字节码描述的信息进行语义分析,以保证其提供的信息符合java语言规范的要求。主要验证
- 该类是否有父类
- 该类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,出现不符合规则的方法重载,例如方法参数都一致,但是返回值类型却不同)等等
- 字节码验证:主要是通过数据流和控制流分析,确保程序语义是合法的,符合逻辑的。
- 符号引用验证:符号引用验证发生在虚拟机将符号引用转换为直接引用的时候,符号引用是对类自身以外的信息进行匹配校验
准备:准备阶段的任务是为类或接口的静态字段分配内存空间,并且默认初始化这些字段的值,这个阶段不会执行任何字节码指令
public static int value = 123;
比如在上面的代码,在此阶段只会将value的值赋值为0,而不是123,
- 在一些特殊情况下,如果字段属性中包含了ConstantValue属性,那么准备阶段变量value就会被初始化为ConstantValue属性指定的值,比如
public static final int value = 123;
编译时,value一开始就指向ConstantValue,所以准备期间value的值就已经是123了。
解析:解析阶段是把常量池内的符号引用转换为直接引用的过程,符号引用就是Class文件中的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量。
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要可以唯一定位到目标即可。符号引用于内存布局无关,所以所引用的对象不一定需要已经加载到内存中。各种虚拟机实现的内存布局可以不同,但是接受的符号引用必须是一致的,因为符号引用的字面量形式已经明确定义在Class文件格式中。
- 直接引用(Direct References):直接引用时直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,同一个符号引用在不同虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么它一定已经存在于内存中了。
- 初始化:执行静态代码块,完成静态变量的赋值,静态字段、静态代码段,字节码层面会生成clinit方法,方法中语句的先后顺序与代码的编写顺序相关
