虚拟机类加载机制_类加载的过程

Java虚拟机中类加载的全过程: 加载、验证、准备、解析和初始化这5个阶段

加载

加载时类加载过程的一个阶段,在加载阶段,虚拟机需要完成3件事

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

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

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

(对于HotSpot虚拟机而言,Class对象比较特殊,虽然是对象,但是存放在方法区里面)

通过全限定名来获取定义二进制文件这条,有多种方式实现

* 从zip包中读取,很常见,最终成为日后jar、ear、war格式的基础

* 从网络中获取

* 运行时计算生成,这个场景使用较多的是动态代理

* 由其他文件生成,典型的场景就是jsp应用,有jsp文件生成对应class类

* 数据库中读取

验证

1. 文件格式验证: 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

2. 元数据验证

对字节码描述的信息进行语义分析,是否符合java语言规范

验证点:

1> 是否有父类

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

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

4> 类中的字段、方法是否和父类产生矛盾(例如覆盖了父类的final字段或者出现不符合规则的方法重载等)

3. 字节码验证

4.  符号引用验证

* 符号引用中的字符串全限定名是否能找到对应的类

* 在指定类中是否存在符合方法的字段描述以及简单名称锁描述的方法和字段

* 符号引用中的类、字段、方法的访问性是否可以被当前类访问

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调下.

首先,这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化随着对象一起分配在java堆中

其次,设置类变量的初始值,指的是数据类型的零值,如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量就会初始化为ConstantValue指定的值,比如

public static final int value = 123 ; 在准备阶段value生成ConstantValue属性,初始化为123而不是零


解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用指的是常量池中的常量(全类名,方法修饰符和方法名,字段修饰符合字段名),在class文件中表示为表形式的常量,例如:CONSTANT_Fieldref_info

什么是符号引用、直接引用?

符号引用:

符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用和虚拟机实现的内存布局无关,引用的目标不一定加载到了内存中,各种虚拟机实现的内存布局可以不同,但是能够接受的符号引用必须一致,因为是定义在class文件中

直接引用:

直接引用是能够指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。有了直接引用,那么引用的目标必定存在内存中

重新回顾下,class文件中常量池中的项目类型,有哪些


1. 类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,虚拟机整个解析需要3个步骤:

1> 如果C不是一个数组类型,那么虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C

2> 如果C是一个数组类型,并且数组的元素类型为对象,按照前面的规则加载数组元素类型,接着有虚拟机生成一个代表次数组维度和元素的数组对象

3> 如果上面的步骤没有出现任何异常,那么c在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认D是否具备对C的访问权限

2. 字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,如果字段所属的接口或者类的符号引用解析异常,就会导致字段解析失败,字段解析的步骤,当前类或接口使用C表示:

1> 如果C中包含简单名称和字段描述符都和目标匹配的字段,直接返回这个字段的直接引用

2> 如果C中实现了接口,将会按照继承关系从下到上递归搜索各个接口和它的父接口,如果接口中字段名和描述符和目标匹配,直接返回它的直接引用

3> 如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中存在字段名和描述符合目标匹配,直接返回它的直接引用

4> 否则,解析失败

最后,如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果不具备对字段的访问权限,将抛出异常。并且如果一个同名字段同时出现在C的接口和父类中,或者在自己或父类的多个接口中出现,那么编译器将可能拒绝编译

3.类方法的解析

首先对方法表中class_index项中索引CONSTANT_Methodref_info所属的类或接口方法的符号引用进行解析, 如果解析成功,才会继续,否则解析失败,类方法解析的步骤如下(使用C表示这个类):

1> 类方法和接口方法符号引用的常量类型是分开的,如果在类方法表中发现class_index索引的C是个接口,解析失败

2> 第一步校验通过后,在C类中查找是否有简单名称和描述符都和目标匹配的方法,如果有,返回这个方法的直接引用

3> 否则,在类C的父类中递归查找是否有简单名称和描述符都和目标匹配的方法,如果有,返回这个方法的直接引用

4> 否则,在类C实现的接口列表以及它们的父接口中递归查找是否有匹配到的,如果存在,说明C是一个抽象类,此时查找结束,抛出异常java.lang.AbstractMethodError

5> 否则,方法查找失败,抛出java.lang.NoSuchMethodError

最后,如果查找过程中返回了直接引用,将会对方法进行权限验证,如果不具有对此方法的访问权限,将抛出异常

4. 接口方法解析

接口方法也需要解析出接口方法表的class_index索引表示的类或接口的符号引用,如果解析失败,则方法解析失败

,成功的话,继续接口方法解析

1> 如果解析class_index出来的是类不是接口,直接就抛异常,解析失败

2> 如果当前接口和父接口中有方法名、描述符和目标匹配的,直接返回它的直接引用

3> 否则,查找失败,抛出java.lang.NoSuchMethodError异常

因为接口中的方法都是public ,不存在访问权限的问题,因此不抛出java.lang.IllegalAccessError异常

初始化

类的初始化就是执行类构造器<clinit>()方法的过程。关于类执行构造器,有几点需要注意:

* 类构造器方法指的是编译器自动收集类中所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。

(静态语句块只能访问到定义在静态语句块前面的变量,定义在语句块后面的变量可以赋值,但是不能访问)

* <clinit>()方法与类的构造函数不同,它不需要显式的调用父类构造器,虚拟机执行时,能够保证父类的<clinit>()必定在子类的<clinit>()方法执行前,就执行完毕,而且最先执行必定是Object的<clinit>()方法

* <clinit>()方法对于类或接口而言,并不是必须的,一个类没有静态语句块,没有类变量,就可以不生成<clinit>()方法

* 接口中不能使用静态语句块,但是接口中仍然存在变量的赋值操作,也会存在<clinit>()方法,只不过子接口执行<clinit>()方法不需要先去执行父接口的<clinit>()方法,只有父接口变量使用时,才会执行

多线程环境下,<clinit>()方法也是线程安全的。如果多个线程去初始化一个类,只会有一个线程执行<clinit>()方法,其他线程会阻塞,直到<clinit>()执行完毕