文章目录
在学习本节之前,务必了解类文件结构
类加载 是什么,从哪里来到哪里去,什么时候产生,是什么过程?
本节针对普通类(排除数组,排除JDK7+支持动态语言的特性相关)的加载过程
基本理解
定义:
一个比较简单的定义如下:
根据类的全限定名(java.lang.Integer)找到类对象(Class对象),然后将它加载到方法区中
过程
类的使用有7个阶段,加载具有5个阶段,我们主要描述的也是这5个阶段
图中按难易程度标注了五个过程
- 简单: 绿色
- 一般: 浅红
- 复杂: 红色,深红色
虚拟机并没有规定类加载的具体时机,只对解析
和初始化
进行了明确的规定.
类加载时机规定
初始化的规定
- 先初始化main方法所在的类
- 父类未初始化时,先初始化父类
- 遇到 new
创建对象new Object()
,putstatic为静态字段赋值
,getstatic获取静态字段的值
,invokestatic执行静态方法
字节码时 - 反射调用类时
- 使用JDK7的动态语言支持的时候. MethodHandler的解析结果为 putstatic,getstatic,invokestatic时 (不管它,反正也不用)
解析的规定
在遇到下列字节码之前进行解析:
- anewarray 创建数组(元素是引用类型)
- checkcast 对象强制转换
- getfield 获取字段的值
- getstatic 获取静态字段的值
- instanceof instanceof 判断
- invokedynamic 动态调用点限定符(目前java不会生成,为了支持其他语言用的)
- invokeinterface 执行接口方法
- invokespecial 执行私有方法,构造方法
- invokestatic 执行静态方法
- invokevirtual 执行虚方法
- ldc 将int、float或String型常量值从常量池中推送至栈顶
- ldc_w 将int、float或String型常量值从常量池中推送至栈顶(宽索引)
- multianewarray 创建多维数组
- new 创建对象
- putfield 设置字段的值
- putstatic 设置静态字段的值
顺序说明
在类加载过程中 加载->验证->准备->初始化 的开始顺序
是确定的。
解析却不一定,由虚拟机实现来决定在初始化之前或者之后执行
可以从字节码规定中发现,以下指令是初始化和解析重合的。
所以笔者认为:在遇到以下4个指令的情况下,由虚拟机的具体实现去决定初始化
和解析
谁先谁后
- new
- putstatic
- getstatic
- invokestatic
图形说明
- 灰色 无关
- 红色 顺序不确定
- 绿色 执行解析
类加载五阶段
类加载的触发有3种情况
- 虚拟机启动加载(不管)
- 由解析阶段触发 (加载,验证,准备 在解析之前)
- 由初始化阶段触发,(加载,验证,准备 在初始化之前)
加载
- 从类的全限定名(java.lang.Integer)获得二进制字节流(byte[])
- 将类静态存储结构(CONSTANT_Class_info)转换为方法区运行时存储结构(虚拟机自己定义结构,没有规范)
- 生成Class对象,作为类的数据的访问接口
加载是一个相对简单的流程,也是程序员最可控的流程,可以由程序员自行决定如何通过类的全限定名获取二进制字节流
- 运行时生成(动态代理)
- 由文件生成(jsp)
- 网络中获取(Applet)
- 压缩文件中获取(war,jar)
验证
验证是比较复杂的一个阶段,与多个阶段都会交叉执行。 (可以用-Xverify:none
关闭验证)
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
这里简单的介绍一下各个验证分别是验证什么内容。 主要说明与哪些阶段交叉,什么时候交叉
文件格式验证
- 文件头是不是0xCAFEBABE
- 主次版本是否在当前版本处理范围内
- 是否有不被支持的常量类型
在加载阶段
的格式转换并存储
时进行验证,验证通过后,才会在方法区存储
这个阶段是对 二进制字节流验证。后面的都是对静态存储结构(StaticClass)
进行验证
元数据验证
- 是否有父类
- 是否继承了final类
- 是否实现了所有抽象方法
在文件格式验证之后
字节码验证
- 不跳转到方法体外
- 操作栈的访问正确性(存int按long去访问)
在元数据验证之后
符号引用验证
- 通过全限定名是否能找到类
- 符号引用中类、字段、方法的访问性(private,protected,public,default)是否可以被当前类访问
在解析阶段中发生
验证小结
省略了加载阶段中,从全限定名获得二进制字节流的过程。
加入了4个验证阶段的发生的时间
准备
这是最简单的一个阶段,只为静态字段赋初始值
非final
静态字段默认初始值:
类型 | 默认值 | 类型 | 默认值 |
byte | 0 | short | 0 |
char | 0 | int | 0 |
long | 0 | double | 0 |
float | 0 | boolean | false |
refrence | null |
例如下面代码的初始值为0
private static int number = 3;
final静态字段
初始值就是定义的值
例如下面代码的初始值就不是0
,而是3
private static final int number = 3;
解析
解析的定义: 将常量池中符号引用替换成直接引用
- 符号引用: 以一组符号来描述所引用的目标,符号引用的字面量明确规定在class文件格式中(目标不一定在内存)
- 直接引用: 能直接或间接定位到目标的指针(目标已在内存)
下图为4种解析的说明
解析针对7种符号引用进行,这里只说明4种(与java有关)。剩余的3种与JDK7新增的动态语言有关
类或接口解析
- 字段不是数组时: 当前类加载器(ClassLoader)去加载这个类型
- 是数组时: 当前类加载器(ClassLoader)去加载元素类型,数组类型有JVM的类加载器去加载
字段解析
- 在当前类查找字段
- 在接口列表和接口的父接口查找
- 在父类中查找
类方法解析
- 查找当前类
- 查找父类
- 查找父接口,找到则抛出异常(java.lang.AbstractMethodError)
- 抛出异常(java.lang.NoSuchMethodError)
接口方法解析
- 当前接口查找
- 父接口查找
- 抛出异常(java.lang.NoSuchMethodError)
初始化
初始化阶段做了以下两件事情
- 合并静态字段、静态块生成类构造器方法
- 执行类构造器方法
执行类构造器()方法(非实例构造)
类构造器是什么?
类构造器由编译器收集静态字段
和静态块
合并产生的。执行顺序和源码顺序一致
例如:
private static int a = 1;
static{
int ma = 2;
}
private static int b = 2;
合并后的类构造器内容
private <clinit>(){
int a = 1;
int ma = 2;
int b = 2;
}
说明
本文的内容全部来自于: 《深入理解Java虚拟机》。