java类加载

1、加载

在加载阶段虚拟机需要完成以下三件事:

通过一个类的全限定名称来获取此类的二进制字节流

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

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

这三件事在Java虚拟机中并没有说的很详细,比如类的全限定名称是如何加载进来的,以及从哪里加载进来的。通常来讲,一个类的全限定名称可以从zip、jar包中加载,也可以从网络中获取,也可以在运行的时候生成(这点最明显的技术体现就是反射机制)。

对于类的加载,可以分为数组类型和非数组类型,对于非数组类型可以通过系统的引导类加载器进行加载,也可以通过自定义的类加载器进行加载。这点是比较灵活的。而对于数组类型,数组类本身不通过类加载器进行加载,而是通过Java虚拟机直接进行加载的,那么是不是数组类型的类就不需要类加载器了呢?答案是否定的。因为当数组去除所有维度之后的类型最终还是要依靠类加载器进行加载的,所以数组类型的类与类加载器的关系还是很密切的。
欢迎家裙 四九九七五四六一四交流,备注cs.
通常一个数组类型的类进行加载需要遵循以下的原则:

如果数组的组件类型(也就是数组类去除一个维度之后的类型,比如对于二维数组,去除一个维度之后是一个一维数组)是引用类型,那么递归采用上面的过程加载这个组件类型

如果数组类的组件类型不是引用类型,比如是基本数据类型,Java虚拟机将把数组类标记为与引导类加载器关联

数组类的可见性与组件类型的可见性是一致的。如果组件类型不是引用类型,那么数组类的可见性是public,意味着组件类型的可见性也是public。

前面已经介绍过,加载阶段与连接阶段是交叉进行的,所以可能加载阶段还没有完成,连接阶段就已经开始。但是即便如此,记载阶段与连接阶段之间的开始顺序仍然保持着固定的顺序。

2、验证

验证阶段的目的是为了确保Class字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。

虚拟机的验证阶段主要完后以下4项验证:文件格式验证、元数据验证、字节码验证、符号引用验证。(结合前文,查看Class类文件结构)

a、文件格式验证

这里的文件格式是指Class的文件规范,这一步的验证主要保证加载的字节流(在计算机中不可能是整个Class文件,只有0和1,也就是字节流)符合Class文件的规范(根据前面对Class类文件的描述,Class文件的每一个字节表示的含义都是确定的。比如前四个字节是否是一个魔数等)以及保证这个字节流可以被虚拟机接受处理。

在Hotspot的规范中,对文件格式的验证远不止这些,但是只有通过文件格式的验证才能进入方法区中进行存储。所以自然也就知道,后面阶段的验证工作都是在方法区中进行的。

b、元数据验证

元数据可以理解为描述数据的数据,更通俗的说,元数据是描述类之间的依赖关系的数据,比如Java语言中的注解使用(使用@interface创建一个注解)。元数据验证主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范(Java语法)的元数据信息。

具体的验证信息包括以下几个方面:

这个类是否有父类(除了java.lang.Object外其余的类都应该有父类)

这个类的父类是否继承了不允许被继承的类(比如被final修饰的类)

如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的方法

类中的字段、方法是否与父类产生矛盾(比如是否覆盖了父类的final字段)

c、字节码验证

这个阶段主要对类的方法体进行校验分析。通过了字节码的验证并不代表就是没有问题的,但是如果没有通过验证就一定是有问题的。整个字节码的验证过程比这个复杂的多,由于字节码验证的高度复杂性,在jdk1.6版本之后的虚拟机增加了一项优化,Class类文件结构这篇文章中说到过有一个属性:StackMapTable属性。可以简单理解这个属性是用于检查类型是否匹配。

d、符号引用验证

这个验证是最后阶段的验证,符号引用是Class文件的逻辑符号,直接引用指向的方法区中某一个地址,在解析阶段,将符号引用转为直接引用,这里只进行转化前的匹配性校验。符号引用验证主要是对类自身以外的信息进行匹配性校验。比如符号引用是否通过字符串描述的全限定名是否能够找到对应点类。

符号引用(Symbolic Reference)
符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可(符号字面量,还没有涉及到内存)。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载在内存中。各种虚拟机实现的内存布局可以各不相同,但是他们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct Reference)
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄(可以理解为内存地址)。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

进行符号引用验证的目的在于确保解析动作能够正常执行,如果无法通过符号引用验证那么将会抛出java.lang.IncomingChangeError异常的子类。

3、准备

完成了验证阶段之后,就进入准备阶段。准备阶段是正式为变量分配内存空间并且设置类变量初始值。

需要注意的是,这时候进行内存分配的仅仅是类变量(也就是被static修饰的变量),实例变量是不包括的,实例变量的初始化是在对象实例化的时候进行初始化,而且分配的内存区域是Java堆。这里的初始值也就是在编程中默认值,也就是零值。

例如public static int value = 123 ;value在准备阶段后的初始值是0而不是123,因为此时尚未执行任何的Java方法,而把value赋值为123的putStatic指令是程序被编译后,存放在类构造器clinit()方法之中,把value赋值为123的动作将在初始化阶段才会执行。

特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的值,例如public static final int value = 123 编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将变量赋值为123。

4、解析

解析阶段是将常量池中的符号引用替换为直接引用的过程(前面已经提到了符号引用与直接引用的区别)。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。

到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为一解析状态),这样就避免了一个符号引用的多次解析。

解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。这里主要说明前四种的解析过程。

a、类或者接口解析

要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤:

如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给调用这个符号引用的类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载

如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载,虚拟机将会生成一个代表此数组对象的直接引用

如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常

b、字段解析

对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:

如果该字段符号引用(简称符号)就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束

否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么直接返回这个字段的直接引用,解析结束

否则,如果该符号所在的类不是Object类,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束

否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常

c、类方法解析

进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:

类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常

如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束

否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束

否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常(因为抽象类是没有实现的)

否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常

d、接口方法解析

同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:

如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常

否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。查找结束

否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
否则,查找失败

5、初始化

初始化阶段,虚拟机才开始真正执行Java程序代码,前文讲到对类变量的初始化,但那是仅仅赋初值,用户自定义的值还没有赋给该变量。只有到了初始化阶段,才开始真正执行这个自定义的过程。

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。

注意以下几种情况不会执行类初始化:

通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

定义对象数组,不会触发该类的初始化。

常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

通过类名获取Class对象,不会触发类的初始化。

通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值