JVM深入浅出(7)--- 虚拟机类加载机制
1. 概述
java虚拟机的类加载过程指的是将二进制的class文件加载,校验,转换解析,初始化的过程。
在java语言中,加载,连接,初始化,这些过程都是在实际运行期完成的。也就会产生格外的内存开销,但是由于动态连接和动态加载的行为也为java提供了很高的动态拓展。
2. 类加载的时机
类型的生命周期:
- 加载
- 连接
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
其中,加载验证准备初始化卸载的顺序是固定的,而解析的时间不一定,为了支持java的动态绑定特性,jvm会在运行期间动态连接,去解析某些符号引用。
初始化的时机
-
关于什么情况下加载是没有约束的,但是对于初始化,jvm有以下六种情况会触发类的初始化,这六种情况也被称为主动引用
-
当触发new,getstatic,putstatic,invokestatic指令时
-
使用new创建实例对象
-
读取或设置静态字段(fianal修饰,已在编译器把结果放在常量池的静态字段除外)
-
调用类型静态方法
-
-
使用java.lang.reflect包进行反射,如果类还初始化的化,会被初始化
-
初始化一个类时,会先初始化它的父类
-
包括Main方法的类会被初始化
-
当java.lang.invoke.MethodHandle实例解析为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,会触发方法对应类的初始化 (相当于将要去调用方法的静态方法)
-
如果类继承了接口的default方法,类被初始化了,这个接口要先被初始化。
-
-
除此之外,所有引用类型的方 式都不会触发初始化,称为被动引用。
- 通过子类引用父类的静态字段,不会导致子类初始化
- 引用数据类型数组创建,不会导致类初始化
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的 类的初始化
对比接口和类:
- 接口和类都可以初始化,编译器都会为他俩生成
<clinit>方法 - 接口不能使用
static{}代码块,但是类可以 - 类被初始化,其父类都要被初始化,但是接口被初始化了,不要求父接口被初始化
3. 类加载的过程
3.1 加载
加载就是反序列化的过程,加载分为以下三步
- 通过完全限定名定位到class文件,获取二进制字节流加载到虚拟机中。
- 在方法区中加载class文件中对应属性,类元数据,常量池,方法表,字段表,将类中静态结构生成方法区中动态结构
- 在堆中生成Java.lang.class对象,用于访问类方法区中的数据。
灵活性:
通过一个类的全限定名来获取定义此类的二进制字节流”这条规则并不是绝对的,二进制字节流获取的方式多样,也就催生了像动态代理,从网络中获取,其他文件生成这些各种各样的技术。
数组类和非数组类的加载
-
非数组类的加载:非数组类的加载是灵活的,可以通过jvm自带的引导类加载器来加载,也可以通过用户自定义的类加载器来加载,通过重写findClass()/loadClass()方法。
-
数组类加载:数组类并不是通过类加载器加载的,是直接由java虚拟机在内存中动态构造出来的,但是数组类的类型最终还是得类加载器来加载。数组将被标识在加载该组件类型的类加载器的类名称空间上
-
如果数组类型是引用类型,通过正常的双亲委派加载策略去加载
-
如果是基本类型,那就是bootstrap类加载器加载
-
加载和连接的部分操作(如字节码格式验证阶段)是交叉进行的,加载还没完成,可能连接就已经开始了,但是先后顺序是不会变的。
3.2 验证
验证主要是以下四点
- 文件格式验证
- class文件魔术是否是0xCAFEBABE
- 版本号是否能被加载
- 检查常量类型是否支持
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
- ....
- 元数据验证
- 主要是检查类元数据:比如该类是否有父类,是否继承了不能继承的类,如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 字节码验证
- 是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的
- 即使如果一个方法 体通过了字节码验证,也仍然不能保证它一定就是安全的
- 为了避免字节码验证耗时过长,把尽可能多的校验辅助措施放到了javac来做
- 符号引用验证
- 通常是在解析的过程中做的,是检查类是否缺少或者禁止访问它依赖的某些外部类,方法,字段等资源。符号引用是为了确保解析能正常执行
- 符号引用中全限定名能否找到类
- 符号的类,字段可否访问
- 类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 通常是在解析的过程中做的,是检查类是否缺少或者禁止访问它依赖的某些外部类,方法,字段等资源。符号引用是为了确保解析能正常执行
3.3 准备
准备是将类的静态属性分配内存并设置初始值的过程。
注意:
- 这里不包括实例对象,实例对象的分配是随着对象一起分配在Java堆中的
- 对静态属性的赋值是在
<clinit>中做的,这里仅仅是设置初始值 - 如果类字段 的字段属性表中存在
ConstantValue属性,在这一步中就会初始化这个值
3.4 解析
解析是将常量池中的符号引用转为直接引用的过程
- 符号引用:符号引用其实就是一些无歧义字面量,描述所引用的目标。
- 直接引用:能直接定位到具体内存位置的引用,可以是指针,句柄,偏移量。
jvm规范未限定解析发生的时间,但是在17个用于操作符号引用的字节码指令字节码指令执行之前,先完成解析。
多次符号解析:
- jvm对某个符号引用的解析可能不止一次,出了
invokedynamic指令外,第一次解析的结果会被缓存下来,之后的所有解析都应该和这个结果相同。 - 对于invokedynamic指令,不同的invokedynamic指令解析的结果可以不同,因为该指令本身是用于动态语言支持。“动态”的含义是指必须等到程序实际运行到这条指 令时,解析动作才能进行。
解析的符号引用种类包括 类或者接口,字段,接口方法,类方法,方法类型,方法句柄,调用点限定
3.5 初始化
初始化就是执行<clinit>的过程,这个函数是虚拟机生成的。
<clinit>的生成是由所有的静态属性赋值操作 和static{}代码块组成的。编译器收集顺序也是根据语句顺序,但是在static块中,可以访问到定义之前的变量,对于之后定义的变量,可以赋值,但是不能访问。
<clinit>不同于<init>(也就是构造方法),它不会隐式调用父类的<clinit>,但是之前说过,在这个类初始化时, 父类<clinit>肯定已经被调用过了。
<clinit>方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的 赋值操作,那么编译器可以不为这个类生成()方法。
接口中不能有static{},并且但是可以有<clinit>,不同于类,接口的<clinit>被调用,父接口的<clinit>可能没有被调用的,除非实现了父接口的default方法(也就是之前说的一种初始化的情况)。
当多个线程初始化一个类时,<clinit>只会被一个线程调用,其他线程将会被阻塞,直到<clinit>方法被执行完。

浙公网安备 33010602011771号