虚拟机类加载机制
一、概述
在Java语言中,类型的加载、链接和初始化等动作都是在程序允许期间才开始。
缺点:编译更麻烦,加载时性能开销更大
有点:扩展性强,灵活性强
二、类加载的时机
一个类从被加载到虚拟机内存再到卸载出内存,它的整个生命周期将会经历以下七个阶段
- 加载
- 验证(链接)
- 准备(链接)
- 解析(链接)
- 初始化
- 使用
- 卸载
各个步骤之间按顺序进行,但不是上一个结束下一个才开始,通常是上一个在执行时下一个也开始执行了(解析阶段除外,解析阶段可以在初始化之后才做,这是为了支持Java语言的动态绑定性)
Java没有对一个类什么时候加载做出规范。但Java规范了什么时候虚拟机需要对一个类进行初始化
而初始化自然JVM需要对类进行加载链接等步骤
六种一定要初始化的情况
-
Class文件中遇到new、getstatic、putstatic、invokestatice四条字节码指令
-
new关键字实例化对象
-
读取或修改某个类中的静态字段(static修饰的字段)注意:是非final修饰的字段
-
调用一个类的静态方法
-
-
使用反射对方法进行调用
-
初始化一个类时会连带着将其父类进行初始化
-
main方法所在的类会被Jvm先加载
-
新特性句柄对应的类型
-
当一个接口中定义了default方法后,它的某实现类被初始化后,接口将会被初始化(普通接口一般不会随着实现类的初始化而初始化)
注:这六种情况称为主动引用,主动引用会触发类的初始化
不会触发类的初始化的情况
- 通过子类调用父类中的静态字段时,不会触发子类的初始化。只会触发父类及父类的父类的初始化
- 声明初始化一个类的数组形式,并不会触发这个类的初始化(因为实际上由Jvm对这个数组生成对应的一个类,这个类直接继承与Object,创建动作由Class文件中的anewarray指令触发)
- 引用一个类的常量,不会触发这个类的初始化(因为实际上在编译阶段编译器就会将这个常量直接放入引用的这个类中,Class文件中不会出现被引用的类的符号引用入口)
三、类的加载过程
3.1、加载
加载阶段虚拟机主要完成三件事
- 通过类的全限定名获取类文件的二进制字节流
- 将字节流中的静态存储结构转换为方法区中的运行时存储结构
- 在虚拟机内存中生成对应的Class对象作为方法区中各个数据的访问入口
类文件被加载进虚拟机时,需要通过类加载器去完成(一个类和类加载器有唯一确定关系),这个类加载器也可以由用户自定义加载方式
加载数组类
- 如果加载的数组其基类----是引用类型则将这个数组与这个类的类加载器关联,用于加载类
- 如果数组类的基类是基本数据类型,则将数组和启动类加载器关联
- (重点)数组的访问权限和其基类的访问权限一致,例如其基类访问权限为default则在外部包中将不能声明该类型的数组
方法区中的存储结构
Java没有对虚拟机中方法区到底以何种形式的数据存储结构来保存类的信息。各虚拟机自行定义
3.2、验证
主要工作:验证Class文件中的信息是否符合规范
验证阶段主要分为以下四个检验动作
文件格式验证
验证字节流是否符合class文件的规范比如
- 是否以魔数开头
- 主次版本是否在当前JVM的支持访问内
- 常量池中的常量是否有不被支持的常量类型
- Class文件中是否有被删除的其他信息
- …
这一步主要是确保Claas文件流中的信息能正确读到方法区中,因为后续的验证都是基于方法区进行的,不再直接读取流
元数据验证
验证字节码中描述的信息是否符合Java语言规范
例如检验
-
这个类是否有父类(Java中除Object类以外都有父类)
-
这个类的是否继承了不被允许继承的类
-
是否是抽象类,是否符合抽象类的定义
-
类中的字段、方法是否和父类发生冲突,方法重载是否正确等
-
…
字节码验证
第二阶段对类中的元数据进行验证后
第三阶段将对类中的方法进行验证即Class文件中的code属性
即使虚拟机对字节码进行再多的验证都不能保证代码运行时能在有限的时间内结束
所以从JDK1.6开始,通过javac编译器和Java虚拟机进行联合优化。具体做法:在方法体Code属性中添加一个名为“StackMapTable”属性,由编译器在编译时来进行类型检查,虚拟机在进行字节码验证时只需要查看StackMapTable属性即可。
符号引用验证
发生在符号引用转化为直接引用时(解析阶段),检查对应的类是否缺少,或者禁止外部访问
主要检验类容
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 符号引用中的类、字段、方法是否允许访问
- …
当项目中使用的代码都被多次验证和使用过,可以使用 -Xverify:none来关闭大部分的验证措施,减少虚拟机加载类的时间
3.3、准备
为类变量赋值(类中的静态变量,static修饰的变量)
赋值为默认值,注意此处的赋值不会按代码中的赋值来进行赋值,而是赋对应类型的默认值,例如
class{
public static int i=10;
}
这个阶段只是会将i赋值为0,而不是10。(常量则直接赋值为初始值)
真正的赋值阶段推迟到初始化阶段
几种类型的默认值
- int:0
- long:0L
- short:0
- char:’\u0000’
- byte:0
- boolean:false
- float:0.0f
- double:0.0d
- reference:null
引用数据类型默认值为null
3.4、解析
主要作用:将JVM常量池中的符号引用替换成直接引用
符号引用
Class文件中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等常量
可以理解为代码中类的类名,字段名,方法名等
直接引用
直接指向目标的指针或者间接定位到目标的句柄。如果直接引用存在,那引用目标也一定已经加载到了虚拟机中
解析时间:解析时机是不固定的,由各虚拟机自行决定,可以在类被加载器加载时即进行常量池的解析也可以在符号引用在被使用时进行。
能触发解析的指令有17个:checkcast、getfield、invokedynamic…对于除invokedynamic以外的指令触发解析时,不论解析多少次其解析结果将完全一致,不论成功失败
常见的几种解析
- 类或接口解析
- 检查类的是否被加载进内存,不存在则将类的全限定名传给对于的类加载器进行加载
- 确认是否具有访问权限
3.5、初始化
类的初始化是这个类加载的最后一个阶段。这个阶段与前面几个阶段主导权在JVM中不同,这个阶段由应用程序决定
本质就是执行编译器编译后生成的类构造器<clinit>的过程。
- 为静态变量赋值(和准备阶段中的赋值不同,这个阶段赋代码中的值)
类构造器是由编译阶段,编译器收集类中类变量的赋值动作和静态代码块的语句合并产生的
收集顺序由源文件中出现的顺序决定。所以如果在静态代码块后声明的静态变量,只能在静态代码块进行赋值但不能访问
类构造器并不是每个类都必须的,当类中没有静态代码块或者没有为静态变量赋值的动作时将不会生成类构造器
**注意:**类构造器只会执行一次
四、类加载器
4.1、类与类加载器
类的唯一性是由类和它对于的类加载器共同确立的
即使是来源于统一类文件的对象,它们的加载器不同时,对象属于的类也不相同
示例
package com.sy.offer;
import java.io.IOException;
import java.io.InputStream;
/**
* @author 沈洋 邮箱:1845973183@qq.com
* @create 2021/7/18-11:26
**/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new ClassLoader(){
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is==null) return super.loadClass(name);
try {
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.sy.offer.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
}
输出:
class com.sy.offer.ClassLoaderTest
false
这里返回false的原因就是JVM中存在两个这个类的类加载器,一个是我们自定义的一个是JVM自己产生的
4.2、双亲委派机制
Java的类加载器主要是三层结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PVIt0BAM-1627275309749)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20210718152502148.png)]
** 下面都是针对Hotspot虚拟机讨论
启动类加载器
这个加载器复制加载<JAVA_HOME>/lib目录下或者是被-Xbootclasspath指定的(并且是虚拟机能识别的jar包,如果直接将外部jar包放到lib目录下,将不会被加载)这部分类加载器的代码底层使用C++书写
扩展类加载器
这个类加载器复制加载<Java_HOME>/lib/ext 目录下的扩展包,用户可以将具有通用性的类库放置到ext目录下以扩展JDK的功能。这部分代码使用Java书写
应用程序类加载器
负责加载用户类路径上所有的类库
双亲委派的工作流程
一个类加载器收到加载类的请求后,会先将加载请求委派给其父加载器,每层加载器都是如此,直到传递到最上层加载器(启动类加载器),只有当父加载器无法加载时,才会重新交还给子加载器进行加载。这也保证了Object、String类等常用类无法被用户自定义覆盖。保证了系统安全性