理解Java类加载的步骤

前言

与在编译时需要进行“连接”工作的语言不同,在Java语言里,类的加载、连接、初始化过程都是在程序运行期间完成的,这种策略虽然牺牲了一小部分性能,但是大大增加了Java的灵活性,Java里天生可以动态拓展的语言特性就是依赖运行期动态加载和动态连接这个特点来实现的。一些热修复框架(如Tinker)、插件化框架也是运用了Java这种灵活的类加载机制来完成设计。

1.类加载的时机

1.一个类从加载到JVM中开始,到卸载出内存为止,一共会经历7个过程,分别是:加载、验证、准备、解析、初始化、使用、卸载。
其中,从加载到初始化会经历三个部分:加载、连接、初始化。
类的生命周期
2.加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,但也只是按顺序开始,什么时候完成是不确定的,因此,这几个阶段很多时候实在交叉混合地运行。而解析阶段开始的时机则不是确定的,有时候是在初始化之后开始,有时候是在初始化之前。
3.加载阶段什么时候开始JVM规范并没有强制规定,这可以有具体的JVM决定,但是什么时候初始化则是有要求的。
JVM规范规定了有且只有5种时机需要立即初始化:
(1)遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成着4条字节码指令最常见java代码场景是:1.使用new关键字实例化对象时候。2.读取或设置一个类的静态字段的时候(被finnal修饰、已在编译期把结果放入常量池的静态字段除外)。3.调用一个对象的静态方法的时候。
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果其父类没有被初始化,则需要先初始化父类。
(4)当虚拟机启动时,需要制定一个类作为主类(包含main方法那个类),则要先初始化这个主类。
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、Invoke_static的方法句柄,并且这个句柄对应的类没有进行过初始化,则需要先触发其初始化。

2.类加载步骤

整体过程如下图所示:
类加载的过程

2.1 加载

在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情:
  (1). 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,比如:压缩包(Jar等)、网络、动态生成、数据库等);
  (2). 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;(即将第一步的二进制字节流,转化为虚拟机所需的格式存储在方法区中)
  (3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2.2 验证

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

2.2.1 验证文件格式

首先是段验证字节流是否符合Class文件格式的规范,并且能被当前版本虚拟机处理,比如:验证Class文件是否以魔数开通;验证主次版本号是否在当前虚拟机处理的范围之内;验证常量池中是否有不被支持的常量类型等。

2.2.2 元数据验证

然后是验证字节码的描述信息是否符合Java语言规范的要求,比如:是否有父类;是否继承了不被允许继承的类;如果不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法。

2.2.3 字节码验证

再然后主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在上一步对元数据信息中的数据类型进行校验后,这个一步会对类中的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。比如:保证跳转指令不会跳转到方法体以外的字节码指令上;保证方法体中的数据类型转换是有效的。(JDK1.6为了优化检查的性能,在给方法体的Code属性表中增加了一项名为“StackMapStack”的属性,用来描述方法体中所有的基本块(按照控制流拆分成的代码块)开始时本地变量表盒操作栈应有的状态,将字节码验证的类型推导转变成了类型检查,从而节省一些时间)。

2.2.4 符号引用验证

最后校验JVM将符号引用转换成直接引用的时候是否存在问题。(注意符号引用转化成直接引用发生在“解析”阶段)
符号引用的验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要检验下列内容:符号引用中通过字符串描述的全限定名是否能够找到对应的类;在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;符号引用中的类、字段、方法的访问性(被访问修饰修饰)是否可以被当前类访问。

2.3 准备

准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配。不过这个需要注意的是分配内存的对象仅仅限于类变量(被static修饰的变量),不包括实例变脸,实例变量是在new的时候才会在堆中分配内存。
并且,这里所谓的初始值指的是JVM为类变量指定的默认值并不是我们手动分配的值。如:
static int a = 666;
在准备阶段a的值会被指定为int的默认值0,热不是666。因为这时候尚未执行任何的Java方法,而把a赋值为666的操作的指令putstatic是在程序被编译后,存放于类的构造器<clinit>()方法之中的,所以只有在类初始化阶段才会执行。
而如果是被final修饰的常量,则在准备阶段就会被赋值,如:
static final int a = 666
在这种情况下,a的值在准备阶段就会被赋值为666。

2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,符号引用在Class文件中是以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等类型常量出现,它与直接引用的关联如下:
符号引用:符号引用是以一组符号类描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定已经加载到内存中。虽然各种虚拟机实现的内存布局不同,但他们接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在了Java迅即规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用的实现是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机中翻译出来的直接引用一般是不一样的。如果有了直接引用,那么引用的目标必然已经在内存中存在了。
需要注意的是,在解析过一个符号引用过后,虚拟机会进行一定的缓存操作,即如果解析成功,将记录对象的直接引用,避免重复解析。但遇到invokedynamic指令时,则不会有这条规则,因为invokedynamic指令的作用就是用于动态语言支持,动态的含义就是等程序执行到这条指令时才进行解析工作。相对来说,其余的指令都是非动态的,即在完成加载阶段的时候就进行解析。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

2.4.1 类或接口的解析

假设:

class D{
    C c = new C();
    //C[] c = new C[];
}

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用时,那虚拟机完成整个解析过程需要以下3个步骤:
1.如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载的过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关的类加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析就以失败告终。
2.如果加载的类C是数组类型,并且数组的元素类型为对象,将会按照1的规则加载数组的元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
3.如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用的验证,确认D是否具备对C的访问权限,如果不具备访问权限,将抛出IllegalAcessError异常。

2.4.2 字段解析

在解析字段之前,首先要对字段所属的类或接口的符号引用进行解析,如果在此过程中遇到任何异常,到会导致字段符号引用解析失败。如果解析成功,则会按照以下步骤解析字段(将这个字段所属的类或接口用C来表示):
1.如果C本身就包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个字段的直接引用,查找结束。
2.否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索个个接口和它的父接口,如果接口中包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个目标字段的直接引用,查找结束。
3.否则,如果类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果父类包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个目标字段的直接引用,查找结束。
4.否则,查找失败,抛出java.lang.NoSuchFileError

如果最后成功返回了直接引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将会抛出java.lang.IllegalAccessError异常。

2.4.3 类方法解析

在解析类方法之前,同样也需要解析出方法所属的类或者接口的符号引用,如果解析成功,将会按照如下步骤来解析:
1.由于类方法和接口方法的符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中的索引C是个接口,那就直接抛出java.lang.IncompatibleClassChangerError。
2.如果通过了第一步,在类C中查找是否有简单“简单名称”和“描述符都与目标相匹配的方法”,如果有,则返回这个方法的直接引用,查找结束。
3.否则,将会按照继承关系从下往上递归搜索其父类,如果父类包含了“简单的名称”和“描述符都与目标相匹配的方法”,则返回这个方法的直接引用,查找结束。
4.否则,将会按照继承关系从下往上递归搜索个个接口和它的父接口,如果接口中包含了“简单的名称”和“描述符都与目标相匹配的方法”,说明C是一个抽象类,查找结束。抛出java.lang.AbstractMethodError异常。
5.否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError。

2.4.4 接口方法的解析

在解析接口方法的时候也是需要先解析出接口方法表中class_index项中索引的方法所属类或接口的符号引用,如果解析成功,依然采用C表示这个接口,虚拟机将会按照如下步骤进行后续的接口方法搜索:
1.如果接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncampatibleClassChangeError异常。
2.否则,否则在接口C中查找是否有”简单方法名“和”描述符都与目标方法相匹配的方法“,如果有,则返回这个方法的直接引用,查找结束。
3.在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有“简单方法名”和“描述符都与目标相匹配的方法”,如果有,则返回方法的引用,查找结束。
4.否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

2.5 初始化

初始化是类加载的最后一步,在这个阶段JVM才就开始真正执行Java的字节码了。
初始化阶段就是执行类加载器<clinit>()方法的过程,<clinit>()方法主要的任务是为静态变量赋实际的值,并且执行静态代码块。它具有以下特性:
1.<clinit>()方法与实例的构造函数<init>()初始化的时机是不同的,类构造器<clinit>()是会比 实例的构造函数<init>()先执行的,并且类构造器不需要手动调用父类的构造器,因为JVM会保证父类的类构造器先于子类执行。
2.由于父类的类构造器先于子类执行,所以父类的静态代码块也会先于子类执行。
3.<clinit>()对于类(包括抽象类)或者接口来说并不是必须的,如果类或者接口中没有静态代码块或者静态变量赋值的操作,JVM也就不会为这个类生成类构造器。
4.接口中不能有静态语句块,但是也会有静态变量赋值的操作,所以也会生成<clinit>()方法,但是不必先执行父接口的<clinit>()方法,只有当父接口的方法的变量使用时,才会调用,有点“按需执行”的意思。
5.JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。如果在其中执行长时间的耗时操作,会造成多个进程阻塞。但需要注意,在同一个类加载器下,一个类型只会被初始化一次。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值