01. 虚拟机的类加载机制
虚拟机的类加载机制
类加载的时机
- 主动引用
- 遇到
new 、getstatic、putstatic、invokestatic 字节码指令,例如:- 使用
new 实例化对象; - 读取或设置一个类的
static 字段(被final 修饰的除外) ; - 调用类的静态方法。
- 使用
- 对类进行反射调用;
- 初始化一个类时,其父类还没初始化(需先初始化父类
) ;- 这点类与接口具有不同的表现,接口初始化时,不要求其父接口完成初始化,只有真正使用父接口时才初始化,如引用父接口中定义的常量。
- 虚拟机启动,先初始化包含
main() 函数的主类; JDK 1.7 动态语言支持:一个java.lang.invoke.MethodHandle 的解析结果为REF_getStatic 、REF_putStatic、REF_invokeStatic。
- 遇到
- 被动引用
- 通过子类引用父类静态字段,不会导致子类初始化;
Array[] arr = new Array[10];
不会触发Array 类初始化;static final VAR
在编译阶段会存入调用类的常量池,通过ClassName.VAR
引用不会触发ClassName 初始化。
也就是说,只有发生主动引用所列出的
类的显式加载和隐式加载
- 显示加载:
- 调用
ClassLoader#loadClass(className)
或Class.forName(className)
。 - 两种显示加载
.class 文件的区别:Class.forName(className)
加载class 的同时会初始化静态域,ClassLoader#loadClass(className)
不会初始化静态域;Class.forName 借助当前调用者的class 的ClassLoader 完成class 的加载。
- 调用
- 隐式加载:
new 类对象;- 使用类的静态域;
- 创建子类对象;
- 使用子类的静态域;
- 其他的隐式加载,在
JVM 启动时:BootStrapLoader 会加载一些JVM 自身运行所需的Class ;ExtClassLoader 会加载指定目录下一些特殊的Class ;AppClassLoader 会加载classpath 路径下的Class ,以及main 函数所在的类的Class 文件。
类加载的过程
类的生命周期
加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载
|<------- 连接 ------->|
|<------------- 类加载 ---------------->|
类的生命周期一共有
加载
加载的3 个阶段
- 通过类的全限定名获取二进制字节流(将
.class 文件读进内存) ; - 将字节流的静态存储结构转化为运行时的数据结构;
- 在内存中生成该类的
Class 对象;HotSpot 虚拟机把这个对象放在方法区,非Java 堆。
分类
- 非数组类
- 系统提供的引导类加载器
- 用户自定义的类加载器
- 数组类
- 不通过类加载器,由
Java 虚拟机直接创建 - 创建动作由
newarray 指令触发,new 实际上触发了[L全类名
对象的初始化 - 规则
- 数组元素是引用类型
- 加载:递归加载其组件
- 可见性:与引用类型一致
- 数组元素是非引用类型
- 加载:与引导类加载器关联
- 可见性:public
- 数组元素是引用类型
- 不通过类加载器,由
验证
- 目的: 确保
.class 文件中的字节流信息符合虚拟机的要求。 4 个验证过程:- 文件格式验证:是否符合
Class 文件格式规范,验证文件开头4 个字节是不是 “魔数”0xCAFEBABE
- 元数据验证:保证字节码描述信息符号
Java 规范(语义分析) - 字节码验证:程序语义、逻辑是否正确(通过数据流、控制流分析)
- 符号引用验证:对类自身以外的信息(常量池中的符号引用)进行匹配性校验
- 文件格式验证:是否符合
- 这个操作虽然重要,但不是必要的,可以通过
-Xverify:none
关掉。
准备
- 描述: 为
static 变量在方法区分配内存。 static 变量准备后的初始值:public static int value = 123;
- 准备后为
0 ,value 的赋值指令putstatic 会被放在<clinit>()
方法中,<clinit>()
方法会在初始化时执行,也就是说,value 变量只有在初始化后才等于123 。
- 准备后为
public static final int value = 123;
- 准备后为
123 ,因为被static final
赋值之后value 就不能再修改了,所以在这里进行了赋值之后,之后不可能再出现赋值操作,所以可以直接在准备阶段就把value 的值初始化好。
- 准备后为
解析
- 描述: 将常量池中的 “符号引用” 替换为 “直接引用”。
- 在此之前,常量池中的引用是不一定存在的,解析过之后,可以保证常量池中的引用在内存中一定存在。
- 什么是 “符号引用” 和 “直接引用” ?
- 符号引用:以一组符号描述所引用的对象(如对象的全类名
) ,引用的目标不一定存在于内存中。 - 直接引用:直接指向被引用目标在内存中的位置的指针等,也就是说,引用的目标一定存在于内存中。
- 符号引用:以一组符号描述所引用的对象(如对象的全类名
初始化
- 描述: 执行类构造器
<clinit>()
方法的过程。 <clinit>()
方法- 包含的内容:
- 所有
static 的赋值操作; static 块中的语句;
- 所有
<clinit>()
方法中的语句顺序:- 基本按照语句在源文件中出现的顺序排列;
- 静态语句块只能访问定义在它前面的变量,定义在它后面的变量,可以赋值,但不能访问。
- 与
<init>()
的不同:- 不需要显示调用父类的
<clinit>()
方法; - 虚拟机保证在子类的
<clinit>()
方法执行前,父类的<clinit>()
方法一定执行完毕。- 也就是说,父类的
static 块和static 字段的赋值操作是要先于子类的。
- 也就是说,父类的
- 不需要显示调用父类的
- 接口与类的不同:
- 执行子接口的
<clinit>()
方法前不需要先执行父接口的<clinit>()
方法(除非用到了父接口中定义的public static final 变量) ;
- 执行子接口的
- 执行过程中加锁:
- 同一时刻只能有一个线程在执行
<clinit>()
方法,因为虚拟机要保证在同一个类加载器下,一个类只被加载一次。
- 同一时刻只能有一个线程在执行
- 非必要性:
- 一个类如果没有任何
static 的内容就不需要执行<clinit>()
方法。
- 一个类如果没有任何
- 包含的内容:
注:初始化时,才真正开始执行类中定义的
类加载器
如何判断两个类 “相等”
- “相等” 的要求
- 同一个
.class 文件 - 被同一个虚拟机加载
- 被同一个类加载器加载
- 同一个
- 判断 “相等” 的方法
instanceof
关键字Class 对象中的方法:equals()
isInstance()
isAssignableFrom()
类加载器的分类
- 启动类加载器(Bootstrap)
- <JAVA_HOME>/lib
-Xbootclasspath 参数指定的路径
- 扩展类加载器(Extension)
- <JAVA_HOME>/lib/ext
java.ext.dirs 系统变量指定的路径
- 应用程序类加载器(Application)
-classpath 参数
双亲委派模型
- 工作过程
- 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器
- 因此,所有的类加载请求,都会先被传送到启动类加载器
- 只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载
- 当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器
- 实现
- 检查该类是否已经被加载
- 将类加载请求委派给父类
- 如果父类加载器为
null ,默认使用启动类加载器 parent.loadClass(name, false)
- 如果父类加载器为
- 当父类加载器加载失败时
catch ClassNotFoundException 但不做任何处理- 调用自己的
findClass() 去加载- 我们在实现自己的类加载器时只需要
extends ClassLoader
,然后重写findClass()
方法而不是loadClass()
方法,这样就不用重写loadClass()
中的双亲委派机制了
- 我们在实现自己的类加载器时只需要
- 优点
- 自己写的类库同名类不会覆盖类库的类