Jvm(55),虚拟机类加载机制----类加载的过程----解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在前一章讲解

Class文件格式的时候已经出现过多次,在Class文件中它以CONSTANT_Class_info、

CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢?

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

Java虚拟机规范的Class文件格式中。

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

虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、

checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、

invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、

putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。对同一个符号引用进行多次解析请求是很常见的事情,除invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果第一次解析失败了,那么其他指令对这个符号的解析请求也应该收到相同的异常。

对于invokedynamic指令,上面规则则不成立。当碰到某个前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic指令也同样生效。因为invokedynamic指令的目的本来就是用于动态语言支持(目前仅使用Java语

言不会生成这条字节码指令),它所对应的引用称为"动态调用点限定符"(Dynamic

Call SiteSpecifier),这里"动态"的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是"静态"的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池的CONSTANT_Class_info、

CONSTANT_Fieldref_info、CONSTANT_Methodref_info、

CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、

CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7种常量类型

[1]。下面将讲解前面4种引用的解析过程,对于后面3种,与JDK 1.7新增的动态语言支持息

息相关,由于Java语言是一门静态类型语言,因此在没有介绍invokedynamic指令的语义之前,没有办法将它们和现在的Java语言对应上,笔者将在第8章介绍动态语言调用时一起分析讲解。

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

  1. 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
  2. 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类

似"[Ljava/lang/Integer"的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是"java.lang.Integer",接着由虚拟机生成一个代表此数组维度和元素的数组对象。

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

2.字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index[2]项中索引的

CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccessError异常。

在实际应用中,虚拟机的编译器实现可能会比上述规范要求得更加严格一些,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译器将可能拒绝编译。在代码清单7-4中,如果注释了Sub类中的"public static int

A=4;",接口与父类同时存在字段A,那编译器将提示"The field Sub.A is ambiguous",不明确,并且拒绝编译这段代码。

代码清单


package
demo.jvm.test6?

public
class
FieldResolution {

interface
Interface0 {

int
A = 0?

}

interface
Interface1 extends
Interface0 {

int
A = 1?

}

interface
Interface2 {

int
A = 2?

}

static
class
Parent implements
Interface1 {

public
static
int
A = 3?

}

static
class
Sub extends
Parent implements
Interface2 {

//public static int A = 4?

}

public
static
void
main(String[] args) {

System.out.println(Sub.A)?

}

}

3.类方法解析

类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index[3]项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。

  1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出

java.lang.IncompatibleClassChangeError异常。

  1. 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  2. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  3. 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出 java.lang.AbstractMethodError异常。
  4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。    后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

4.接口方法解析

接口方法也需要先解析出接口方法表的class_index[4]项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。

  1. 与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
  2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  3. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛java.lang.IllegalAccessError异常。

从上面我们可以看到结果是解析就是我们平常在eclipse中看到的编译的过程,假如编译有问题就会报红了。

原文地址:https://www.cnblogs.com/qingruihappy/p/9691476.html

时间: 2024-10-14 06:35:03

Jvm(55),虚拟机类加载机制----类加载的过程----解析的相关文章

虚拟机类加载机制——类加载时机

由于道行不够深,所以此篇类加载机制的讲解主要来自于<深入理解Java虚拟机——JVM高级特性与最佳实践>的第7章 虚拟机类加载机制. 在前面<初识Java反射>中我们在开头提到要了解Java反射,就得要了解虚拟机的类加载机制.在这里,我们来试着窥探一下何为类加载. “虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,类型的加载.连接和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.”这句话确实读着好懂,但到底类加载做了什么呢?我们都知道

Jvm(56),虚拟机类加载机制----类加载的过程----初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制.到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码). 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是 执行类构造器<clinit>()方法的过程.我们在下文会讲解<clinit>()方法是怎么生成的

Jvm(51),虚拟机类加载机制----类加载的时机

在了解下面的举的例子之前我们先来了解一下类的加载顺序? 1 public class test1 { 2 public static void main(String[] args) { 3 C c = new C(); 4 } 5 } 6 7 class A{ 8 int a = 0; 9 Method m = new Method(a); 10 static int a1 = 10; 11 static{ 12 System.out.println("A:执行静态代码块A"+a1)

JVM类加载机制---类加载的过程

一.类加载的时机 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用.卸载 7个阶段,其中验证.准备.解析 3个部分统称为 连接. 二.具体步骤解析 1.加载 加载阶段,虚拟机要完成以下3件事情: 1)通过一个类的全限定名来获取定义此类的二进制字节流: 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构: 3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口. 相对于类加载过

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

1.加载 虚拟机需要干三件事: ①.通过一个类的的全限定名来获取定义此类的二进制字节流(没有规定二进制字节流从那里获取,怎样获取,许多java技术也都建立在这基础上) ②将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(将常量池转变成运行时常量池) ③在内存中生成一个代表这个类的java.lang.Class对象,作为方法区着各类的各种数据的访问入口. 相比较于类加载过程的其他阶段,非数组类获取类的二进制字节流的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器

虚拟机类加载机制--类加载器

准备阶段的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何如获取所需要的类.实现这个动作的代码模块称为"类加载器" 1.类与类加载器 每一个类加载器都有一个独立的类名称空间,由类加载器和类一起合作才能确定一个类在虚拟机中的唯一性.也就是说:比较两个类是否"相等",即使他们来自同一个Class文件,在同一个虚拟机上被加载,如果加载它们的类加载器不同,那么这两个类就不相等. 这里的

Java虚拟机 - 类加载机制

[深入Java虚拟机]之四:类加载机制 类加载过程     类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).另外注意这里的几个阶段是按顺序开

Java 虚拟机程序执行:02 虚拟机的类加载机制

虚拟机的类加载机制 虚拟机的类加载机制 类加载的时机 类的显式加载和隐式加载 类加载的过程 类的生命周期 加载 加载的 3 个阶段 分类 验证 准备 解析 初始化 类加载器 如何判断两个类 “相等” 类加载器的分类 双亲委派模型 类加载的时机 JVM 会在程序第一次主动引用类的时候,加载该类,被动引用时并不会引发类加载的操作.也就是说,JVM 并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次.那么什么是主动引用,什么是被动引用呢? 主动引用

两道面试题,带你透彻解析Java类加载机制

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.ou