【JVM】类加载时机与过程
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。下面来总结梳理类加载的五个阶段。
类加载发生在程序运行期间,会有一些性能开销,但是会提供灵活性,Java动态扩展的特性就是依赖运行时期动态加载和动态连接特点
类加载分为五个阶段:
- 加载
- 验证
- 准备
- 解析
- 初始化
后四个阶段统称为“连接”阶段
加载
加载阶段,虚拟机完成以下三件事:
- 通过一个类的全限定名来获取此类的二进制字节流(即class文件);
- 将字节流的静态存储结构转化为方法区的运行时数据结构;
- 内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口;
注意:
- 虚拟机规范中未规定方法区中的运行时数据结构,由虚拟机自行定义;
- 实例化的java.lang.Class对象未明确规定存储在堆中,HotSpot中的class对象存在方法区中
加载阶段尚未完成时,后续连接解读那可能已经开始
验证
验证阶段主要是为了确保class文件字节流中的信息符合虚拟机要求,不会危害虚拟机自身安全。主要有以下几种格式验证:
- 文件格式验证:验证是否符合Class文件规范
- 元数据验证:字节码语义分析,是否符合Java语言要求
- 字节码验证:验证类的方法体,保证运行不会产生危害
- 符号引用验证:验证符号引用是否能找到对应的类,是否具有访问权限等
对于反复使用和验证过的代码,可以使用-Xverify:none 可以关闭类验证措施,以缩短类加载时间。因为平时开发中idea和eclipse等工具的验证功能很完善,能保证代码准确。
准备
为static变量分配空间,不包括实例变量,实例变量会在对象实例化时和对象一起分配在堆中,
- static分配空间在“准备”阶段(在方法区中分配),赋值在“初始化”阶段。例如 public static int value = 123 会在准备阶段设置初始值0,在初始化阶段赋值“123”;
- static+final修饰基本数据类型和字符串常量时,分配空间和赋值都是在准备阶段,可以借此优化代码
- static+final修饰引用类型,即用new初始化时,是在构造方法中分配空间和赋值,即在初始化阶段完成
解析
将常量池中的符号引用解析为直接引用。那么问题来了,什么是符号引用和直接引用:
- 符号引用:描述目标的符号,与虚拟机内存布局无关,以字面量的形式明确定义在class文件中。Java虚拟机内存布局各不相同,但是符号引用必须一致。Java类编译时,不知道引用的类的实际地址,因此使用符号引用。
- 直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。
解析动作针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:
常说的“类加载时机”与“初始化时机”一一对应。因为要进入初始化阶段,就必须要完成加载、验证、准备、解析等阶段。
初始化发生时机如下:
- main方法所在类总是会被首先初始化
- 首次访问这个类的静态变量或静态方法时会导致初始化
- 子类初始化会联动父类初始化
- 通过子类访问父类静态变量,只会触发父类初始化
- Class.forName
- new会导致初始化
以下情况不会导致类初始化的情况:
- static final不会触发初始化
- 访问类对象.class
- 创建该类的数组
- 类加载器的loadClass方法
- Class.forName的参数2为false
初始化阶段实际上是执行类构造器<clinit>()
方法的过程。<clinit>()
会根据源文件中顺序收集类中所有类变量赋值动作和static块语句
注意:static块能访问代码块之前的变量,之后的变量可以赋值,但不能访问
<clinit>()
方法的执行有如下特点:
- 第一个执行clinit方法的一定是Object类
- 父类中静态语句块一定优先于子类
- 如果类中没有static方法和变量,不会产生clinit方法
- 接口中如果有static变量,也会生成clinit,但是子接口不会执行父接口中的clinit,接口的实现类也不会调用接口中的clinit
- 多线程时,只会有一个线程会去执行clinit,会加锁,保证线程安全