类加载机制
一、概念
在前面我们学习了Class文件存储格式的具体细节,在Class文件中描述的各类信息,最终都需要加载到虚拟机中才能被运行和使用。而虚拟机是如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?是我们这次需要学习的内容
与那些在编译时需要进行连接的语言不同,在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性。Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以直接被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制
二、Loading–加载
加载
加载是类加载机制的第一个过程,在加载阶段,虚拟机主要完成三件事:
- 通过一个类的全限定名来获取其定义的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口
类加载器和双亲委派机制
Java虚拟机团队有意把类加载阶段中的“通过一个类的全限定名来描述该类的二级制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为”类加载器“(Class Loader)
类加载器在类层次划分、OSGI、程序热部署、代码加密等领域大放异彩,成为Java体系中的一块重要基石
加载阶段完成之后,Java虚拟机外部的二进制文件字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的数据存储格式完全由虚拟机实现自行定义
类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class对象,这个对象将作为程序访问方法区中的类型数据的入口
三、Linking–连接
前置知识:Load过程中进行内存分配的时候,仅包括类变量,而不包括实例变量;实例变量将会在对象实例化时随着对象一起分配在Java堆中
类变量:属于类的变量,通俗来讲就是被 static 修饰的变量
实例变量:属于类的实例的变量,也就是说是属于不同的实例对象的
1、验证——Verification
验证是为了确保Class文件中包含的信息符合约束要求,保证这些信息被当成代码运行后,不会危害JVM的安全。校验是非常重要的,这个阶段是否严谨,直接决定了JVM是否能够承受恶意代码的攻击
总共完成四个阶段的检验动作:
1、文件格式验证
检验文件是否符合JVM规定格式:是否是以魔数0xCAFEBABE开头
主、次版本号是否在当前虚拟机接受范围之内
…等等
此阶段验证是基于二级制字节流进行的,此后这段字节流就会进入方法区进行存储,所以后面的验证阶段都是基于方法区的存储结构上进行的
2、元数据验证
3、字节码验证
4、符号引用验证
2、准备——Preparation
准备阶段是正式为类中定义的变量(即静态成员变量,被static修饰的变量),分配内存并设置类变量初始值的阶段
这一步很重要,面试可能会考
比如:
public static int i = 8;
到这个步骤,会把static变量赋零值(初始值),也就是说此时 i = 0,还没有把 8 赋值给 i
基本数据类型的零值
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
byte | (byte)0 | reference | null |
char | ‘\u0000’ |
3、解析——Resolution
解析阶段是Java虚拟机将常量池中内的符号引用替换为直接引用的过程
1、将类、方法、属性等符号引用解析成直接引用
2、将常量池中的各种符号引用,解析成指针、偏移量等内存地址的直接引用
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符这7类符号引用进行。《深入理解JVM虚拟机》P274
四、Initialization–初始化
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码进行的主观计划去初始化类变量和其他资源
初始化过程
public static int i = 8;
在上面Linking过程,我们知道int处于半初始化状态,i 为默认值 0 ,在此步Initialization过程,就会把初始值8赋值: i = 8
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K5OoNb4L-1596637180449)(imges/%E5%88%9D%E5%A7%8B%E5%8C%96.png)]
小总结:
class --》 load --》 类变量赋默认值 --》 赋初始值
new --》申请内存 --》实例变量赋默认值 – 》赋初始值
由于存在半初始化过程,这是比较恐怖的;
由于CPU为了提高效率,存在指令的乱序执行,如果多线程情况下没有进行并发安全的控制,是会发生变量在半初始化状态(零值)就被另一个线程使用,造成数据错乱(如订单数置零)的情况,所以加锁操作很重要
《JAVA虚拟机规范》中,严格规定了有且只有六种情况必须对类进行初始化:《深入理解JVM虚拟机》P263
参考:《深入理解Java虚拟机》第7章
Process On: