JVM类加载机制

一 定义

虚拟机把描述类数据的Class文件加载到内存,并对数据进行 校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程就是类加载机制。

在Java中,类的加载、连接、初始化都是在运行期间完成的,这种策略稍微影响程序运行的性能,但是增加了Java程序的灵活性。


二 类的生命周期

类从被加载到虚拟机内存开始,到卸载出内存,其生命周期为

加载、连接(包括验证、准备、解析)、初始化、使用、卸载。总共七个阶段。

其中,加载、验证、准备、初始化、卸载的顺序是固定的,而解析不固定。在某些情况下,解析可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也叫动态绑定或者晚期绑定)。


三 类加载的时机

1 对于何时执行“加载”操作,虚拟机没有规定。但是对于“初始化”操作(加载肯定在这之前完成),虚拟机有明确规定。有且只有以下五种情况:

(1)遇到new、putstatic、getstatic、invokestatic这四条字节码指令时。需要触发初始化。对应的场景是使用new创建对象、获取或者设置类的静态属性、调用类的静态方法。

(2)使用java.lang.reflect包的方法对类进行反射时。

(3)当初始化一个类,但是其父类没有初始化时,初始化其父类。

(4)虚拟机启动时,用户指定的主类需要被加载。

(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic,REF_putStatic、REF_invokeStatic的方法句柄,而方法所属的类没有初始化时,需要初始化类。


以上五种情况称为对类的“主动引用”,会触发类的加载;其他情况属于对类的“被动引用”,不会触发类加载。


2 被动引用的情况

(1)子类引用父类的静态字段:只会初始化父类,不会初始化子类。

(2)创建一个类的数组:JVM只会自动创建并初始化一个对应的数组类(对数组元素的一层包装),不会初始化数组元素对应的类。

(3)一个类引用另外一个类的常量值:编译阶段通过常量传播优化,常量值已经被存入调用类的常量池,因此不会初始化被调用类。


四 类加载的过程

类加载的全过程,包括加载、验证、准备、解析、初始化五个阶段


五 加载

加载完成了三件事

第一,通过类的全限定名来获取定义此类的二进制字节流。

第二,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

第三,在内存中生成一个java.lang.Class对象(虽然是对象,但是存储在方法区中),作为方法区这个类的各种数据的访问入口。


1 非数组类的加载

这是一个开发人员可控性最强的阶段,非数组类,可以使用系统加载器加载,也可以使用自定义加载器加载。开发人员可以通过自定义的类加载器,去从不同的源获取到表示类的二进制流。常见的来源有:

(1)zip包:jar包,war包,Ear包

(2)从网络中获取

(3)运行时计算生成:动态代理

(4)由其他文件生成:JSP

(5)从数据库中读取


2 数组类的加载

数组类本身不使用类加载器加载,它是由JVM自动生成的。但是数组中的元素对应的类型,需要使用类加载器加载。

数组类的加载原则如下:

(1)如果元素类型是引用类型,那么使用正常的类加载方式,加载元素类。数组类将于加载该元素类的类加载器关联。

(2)如果元素类型是普通类型(例如int[]),那么JVM会将数组标记为与引导类加载器关联。

(3)数组类的可见性与其元素类的可见性一致,如果是普通类型,数组类可见性默认为public。


六 验证

验证是连接阶段的第一步,目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。

Java语言本身是一个安全的语言(例如无法访问数组边界外的数据,无法将一个对象转型成另一个完全不同类型的对象等),但是Class文件字节流来源很多,加载时并不能保证Class字节流中没有“危险操作”。因此,验证阶段很重要。


验证大致完成4个阶段的工作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1 文件格式验证

验证字节流是否符合Java要求的Class文件格式规范。

2 元数据验证

语义分析,在类的层面,验证类的元数据是否符合Java语义规范。

例如是否有父类、是否继承了不可继承的父类等等。

3 字节码验证

同样是语义分析,但是是方法层面的验证,保证方法不会做出危害虚拟机的操作。

例如跳转指令是否跳转到其他方法体、错误的转型操作等。

4 符号引用验证

发生在解析阶段(虚拟机将符号引用转化为直接引用),符号引用是对除了类自身外的信息进行验证,确保类在引用其他内容是不出问题。

符号引用的目的是保证解析可以正常运行。


七 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段。

这里设置的是类变量的值(static),通常情况下初始值设置为对应类型的默认值。只有在对象是final类型时,才会使用开发人员设置的初始值。


八 解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。

虚拟机规范并没有规定解析阶段发生在何时,因此,虚拟机可以根据需要选择在类加载的时候进行解析操作,或者是在一个符号引用将要被用到时,再进行解析操作。

1 符号引用和直接引用

(1)符号引用是一组描述所引用目标的一组符号,与虚拟机的内存无关,引用的目标并不一定已经加载到内存中,只要能无歧义的定位到目标即可。

(2)直接引用是一个指向目标的指针、偏移量或者是句柄。如果有了直接引用,那么引用的目标必然已经存在于内存中了。


2 解析的对象

解析的动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。


3 类或接口解析

(1)如果引用类型不是数组,那么虚拟机使用引用所属类的类加载器去加载引用的类型。

(2)如果引用类型是数组,并且数组元素是类,那么JVM将使用(1)的方式加载元素类,然后生成一个数组对象。

(3)完成引用验证,验证当前类是否具有对引用的访问权限。


4 字段的解析

(1)首先对字段所属的类或接口进行解析

(2)判断当前类是否包含简单名称和字段描述符与目标匹配的字段。

(3)否则,如果对象实现了接口,那么从下向上沿着接口查找。

(4)否则,如果对象有父类,从下往上沿着继承关系查找。

(5)否则解析失败。


5 类方法解析

(1)解析方法所属的类,如果该类是个接口,抛出异常。

(2)检查当前类中是否有简单名称和描述符都匹配的方法。

(3)否则,在父类中递归查找匹配的方法。

(4)否则,在实现接口的列表中递归查找,如果找到,说明当前类是一个抽象类,抛出异常。

(5)否则解析失败。


6 接口方法解析

(1)解析方法所属接口,如果发现是一个类,抛出异常。

(2)在当前接口中查找是否有简单名称和描述符都匹配的目标。

(3)否则,递归在父接口中查找。

(4)否则,解析失败。


九 初始化

初始化时类加载的最后一个阶段,到了这个阶段,JVM才真正开始执行Java程序代码。

在准备阶段,变量已经被赋值为类型对应的默认值。在初始化阶段,则根据程序员的主观设计进行赋值,实质上执行了一个<clinit>方法。

<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。

注意:静态块只能访问获取定义在其之前的变量值。(赋值操作无所谓)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值