JVM学习——虚拟机类加载机制
1 概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。
2 类加载的时机
2.1 类的生命周期
从被虚拟机加载到内存开始,到卸载出内存为止,整个生命周期:
加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始(互相交叉的运行)
,但是解析阶段则不一定:
可以在初始化阶段之后再进行(动态绑定)
2.2 类进行初始化的5种情况(主动引用)
- 遇到new、getstatic(读取静态字段
已在编译期把结果放入常量池的静态字段除外
)、putstatic(设置静态字段)或invokestatic(调用一个类的静态方法)这四条字节码指令时,如果没有进行过初始化,则初始化。 - 对类进行反射调用的时候
- 当初始化一个类的时候,如果其父类还没有进行初始化,则先触发父类的初始化
(如果是接口,并不要求其父接口全部完成了初始化,只有在使用的时候才会被初始化)
- 虚拟机启动时,用户指定的执行主类(main方法),虚拟机会先初始化主类、
- 当使用动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,方法句柄所对应的类没有初始化,则触发其初始化
2.3 举个栗子(被动引用)
例1:通过子类引用父类的静态字段,不会导致子类初始化
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 ExampleTest {
public static void main(String[] args) {
System.out.println(SubClass.value);
//输出结果为
//superClass init
//123
}
}
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
例2:通过数组定义使用类,不会触发此类的初始化
public class Example2Test {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];//没有输出
}
}
没有输出说明并没有触发xxx.xxx.SuperClass的初始化阶段(也就是没有执行()方法)。但是却触了另一个类的初始化
,这个类就是[Lxxx.xxx.SuperClass,这是由虚拟机自动生成的,继承java.lang.Object类的子类,创建动作由字节码指令newarray触发。这个类代表了SuperClass的一维数组,数组中应有的属性和方法都实现在这个类中。
例3:常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
public class ConstantClass {
static {
System.out.println("Constant init");
}
public static final String HELLO = "hello";
}
public class Example3Test {
public static void main(String[] args) {
System.out.println(ConstantClass.HELLO);
//输出结果为:hello
}
}
没有输出"Constant init",这是因为在编译阶段通过常量优化,将常量的值放入了Example3Test类的常量池中,以后对该常量的访问都转化为自身对常量池的引用了。实际上Example3Test的Class文件之中并没有ConstantClass类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系
3 类加载的过程
3.1 加载
在加载阶段,虚拟机需要完成的三件事:
- 通过一个类的全限定名类获取定义此类的二进制流
- 将这个字节流代表的
静态存储结构
转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象(
HotSpot中存放在方法区
),作为方法区这个类的各种数据的访问入口
3.2 验证
3.2.1 文件格式验证
一部分验证点:
- 是否以魔数0xCAFEBABE开头
- 主、次版本号是否在虚拟机处理范围之内
- 常量池中的常量是否有不被支持的常量类型(检查常量的tag标志)
只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会直接操作字节码
3.2.2 元数据验证(语义分析,是否符合Java语言规范)
3.2.3 字节码验证
一部分验证点:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:
在操作数栈中放置一个int类型的数据,使用时却按照long类型来加载入本地变量表
- 保证跳转指令不会跳转到方法体以外的字节码指令上
3.2.4 符号引用验证(发生在虚拟机将符号引用转化为直接引用时,解析阶段发生。可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验)
3.3 准备
准备阶段是正式为类变量(static修饰的变量,实例变量将会在对象实例化时分配在java堆中)
分配类存并设置类变量初始值
的阶段(初始值通常
是数据类型的零值,特殊情况
是被加上final修饰的数据初始值就是指定的值,如public static final int value = 123),变量所使用的内存在方法区中进行分配。
3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号引用以一组符号来描述所引用的目标,引用的目标不一定已经加载到内存中。
- 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄
注意
:虚拟机对解析进行缓存,对符号引用的第一次结果进行缓存(在运行时常量池中记录直接引用,并把常量标为已解析状态),从而避免重复解析(对invokeddynamic指令,上面规则不成立,必须等到代码实际运行到这条指令)
3.4.1 类或接口的解析
3.4.2 字段解析
3.4.3 类方法解析
3.4.4 接口方法解析
3.5 初始化
初始化阶段是类加载过程的最后一步,根据程序员通过程序制定的主观计划去初始化类变量和其他资源(执行()方法的过程)
3.5.1 <clinit>()方法
- <clinit>()方法是有编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器的收集顺序源文件的顺序(静态语句块只能访问定义在静态语句块前的变量,后面的变量可以赋值但是不能访问)
- <clinit>()方法与类的构造函数不同,不用显示的调用,虚拟机保证在子类的方法执行前,父类的方法已经执行完毕,最先的肯定是object
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中会被正确的加锁、同步,如果多个线程同时初始化一个类,只会有一个类执行相应的方法
3.5.2 不被初始化的例子
- 通过子类引用父类的静态字段,子类不会被初始化
- 通过数组定义来引用类
- 调用类的常量