动态链接和解析

每一个class都有一个常量池, 保存它自己的所有的符号引用。
每一个已经被加载的class, interface都另外有一个内部版本的常量池, 叫做runtime constant pool(运行时常量池), 运行时常量池对应class常量池, 因此在一个类型被初始加载后, 所有的这个类型的符号引用都保存在运行时常量池中。

Resolution即找到符号引用标识的实体并替换为直接引用的过程。 因为所有的符号引用都在常量池中, 因此这个过程也叫constant pool resolution(常量池解析)。

使用符号引用的虚拟机指令指定符号引用在常量池中的索引。 比如getstatic指令把一个static属性放入栈中, getstatic后面即跟着二进制的常量池中的索引值。

在相同的或不同的方法中, 可能存在对常量池中同一个条目的索引 但是每一个常量池项只会被解析一次, 后面的引用直接使用解析后的直接索引。

不同的Java虚拟机实现可以选择在程序执行的不同时间进行解析, 主要有两种解析方式:

  • JVM选择在一开始就从初始的类中解析出所有的符号引用, 这样在执行main()方法之前程序就已连接完毕, 也叫early resolution。
  • JVM选择在class被第一次使用的时候才进行符号引用的解析, 也叫late resolution。

在HotSpot虚拟机实现中, 采用的是late resolution, 即在类被第一次使用的时候才会被解析。

常量池解析

CONSTANT_Class_info解析

这是一个常量池中最复杂的类型, 代表类或接口的符号引用。
它的解析规则根据它是否是一个数组类型和引用类型是否是被bootstrap class loader或用户自定义类加载器加载而不同(在JVM实现中, 每一个数组类型对应一个内部的class, 数组的各项操作, 也就是在调用这个class的方法)。

数组类解析
如果一个CONSTANT_Class_info类型的name_index指向一个以左括号([)开头的CONSTANT_ut8_info字符串, 那么它是一个数组类。
如果当前类加载器被记录为被解析的数组类的initiating loader, 那么就使用这个数组类, 否则它会先用当前的类加载器解析数组元素类型, 然后创建一个class代表这个数组类型。对于一个引用类型数组, 它的类加载器被标记为元素的类加载器, 对于一个原始类型数组, 它的类加载器被标记为bootstrap class loader。

非数组类或接口解析
一个CONSTANT_Class_info类型的name_index指向的CONSTANT_utf8_info字符串不以左括号([)开头则是一个非数组类或接口。 那么, 它的解析步骤则较会分为几个步骤,首先加载这个类型, 然后验证对这个类型的访问权限。

对一个类型进行解析, 首先JVM会检查这个类型是否被加载进当前类加载器的命名空间。 为了能够确认当前类加载器是否是一个指定全限定名的类型的intiating loader, 每一个类加载器会维护一个已加载类型的列表, 这个列表组成JVM中的一个个命名空间。类加载器会通过检查与它对应的命名空间中是否有指定的类型来确定一个类型是否已经被加载, 若已经被加载, 它就会使用已经加载的类型, 一个类型的表现为对应方法区的一个数据块的class实例。JVM通过这个机制来保证一个给定名称的类型只会被一个类加载器加载一次(由于每一个类加载器有它自己的命名空间, 因此一个相同的类被不同的类加载器加载, 它们的类型是不相等的)。

如果一个类全限定名未被加载进当前类加载器的命名空间, 那么虚拟机就会把全限定名传递给当前类加载器, 虚拟机总会让当前类加载器作为defining loader通过引用类型的常量池中被解析的的CONSTANT_Class_info去尝试加载被引用的类型。 一个引用类型引用另外一个引用类型, 如果引用类型的defining class loader是bootstrap class loader, 那么虚拟机就会让bootstrap class loader去加载被引用的类型, 否则就会使用用户定义的类加载器去加载被引用的类型。

当一个bootstrap class loader或user-defined class loader加载一个类型时, 它们可以自己加载这个类型或者委托给其他的类加载器。 若委托给一个user-defined class loader, 通过调用那个class loader的loadClass()方法, 并传入需要加载的类全限定名。 若委托给bootstrap class loader, user-defined class loader通过调用ClassLoader的静态方法findSystemClass()并传入需要加载的类的全限定名。 同样, 被委托的类加载器也可以选择自己加载这个类型或者委托给其他的类加载器。 如果一个类加载器成功加载一个类型, 它会被标记为这个类型的defining class loader, 在这个过程中涉及到的所有的类加载器包括defining class loader以及被委托的类加载器被标记为这个类型的initiating loaders。 但是在这其中尝试加载这个类型但失败的类加载器不会被标记为这个类型的initiating loaders。

当一个user-defined class loader的loadClass()方法可以产生一个java class格式的字节码数组时, loadClass()方法必须调用defineClass()方法并传入全限定名以及字节码数组的引用类型。 调用defineClass()方法会让虚拟机把字节码存储到方法区, 并校验字节码数组是否代表声明的全限定名的类型。

当一个引用类型被加载并且不是java.lang.Object类型时, 虚拟机会通过类的全限定名检查它的父类是否被加载进当前类加载器的命名空间, 如果没有, 虚拟机会加载这个类型的父类, 一旦它的父类被加载, 虚拟机会再次检查它的二进制赎金来找到它的父类并加载, 直到一个类型的父类为Object, Object类型没有父类。

然后虚拟机会从Object类型往下检查, 检查每一个类型是否实现了接口类型并确保实现的接口类型被加载, 虚拟机然后检查被加载的接口类型的二进制数据, 检查它是否继承了其他的接口, 然后这样递归检查, 直到所有的接口类型被加载。

当一个类型以及它的所有父类型被成功加载, 虚拟机就会返回一个class实例代表这个类型。 即一个类加载器loadClass()方法通过这个类的defineCLass()方法或findSystemClass()方法或者其他类加载器的loadClass()方法获得class实例, 然后返回给它的调用者。

Step 1 检查访问权限

当加载完毕后, 虚拟机会检查访问权限, 如果当前类型不具有引用的类型的访问权限, 虚拟机会抛出IllegalAccessError。 这个步骤不是标准校验步骤的一部分, 但是是在类被加载后总是被执行的一个步骤。 当这个检查结束后, 整个CONSTANT_Class_info类型的解析也就结束了。

这时候CONSTANT_Class_info引用的类型已经被加载了, 但是还没有被连接或初始化。 并且它的所有父类型也不是要求必须已经被连接或初始化。 但是有的父类型可能已经在更早的解析过程之中被初始化过了。

step 2 验证类型

验证过程中需要虚拟机已经加载类这个类型, 来确保这个类型的字节码符合Java语言的语法。 比如如果一个特定的class的引用类型被赋值为一个不同的class类型, 虚拟机就会加载两个类型来确定是否其中一个类型为另一个的子类。在这个阶段, class可以被连接, 但不会被初始化。

step 3 准备类型

当一个类型的验证阶段结束后, 它也必须被准备好。 在准备阶段, 虚拟机会给class变量分配内存并且准备方法表等数据结构。

step 4 解析类型

这时, 类型已经被加载, 验证, 准备好了, 虚拟机可以在这时候解析类型, 解析的是符号引用中包含的引用类型, 当前引用类型。

step 5 初始化类型

进行类型的初始化,由两步组成, 从顶到下的形式执行类型的初始化, 即执行父类的初始化方法, 和当前类型的初始化方法。

CONSTANT_Fieldref_info解析

虚拟机首先解析class_index中的CONSTANT_Class_info项, 如果CONSTANT_Class_info解析完成, 虚拟机在类型以及其父类中查找指定的field。

  1. 虚拟机首先检查被引用类型的属性, 如果找到则成功。
  2. 否则, 虚拟机在它实现的接口中查找指定的属性, 若找到则成功。
  3. 否则, 如果这个类型有直接父类型, 虚拟机会检查直接父类中查找属性, 找到则成功。
  4. 否则查找失败, 返回NoSuchFieldError, 如果找到但没有权限则抛出IllegalAccessError。

查找成功, 虚拟机会标记这个entry为已解析, 并且在常量池中放入直接引用。

CONSTANT_Methodref_info解析

虚拟机首先解析class_index中的CONSTANT_Class_info项, 若CONSTANT_Class_info解析成功, 虚拟机在解析的类型以及解析类型的父类型中检查是否有指定方法以及是否有访问权限。

  1. 若被解析的是一个接口, 而不是一个类型, 虚拟机抛出IncompatibleClassChangeError.
  2. 否则, 若被解析的是一个class类型, 虚拟机检查是否有指定名称以及描述符的方法, 找到则成功.
  3. 否则, 在被解析的类型的直接父类型以及所有的父类型中递归检查是否有指定方法以及描述符, 找到则成功.
  4. 否则, 检查所有的接口中是否有指定方法以及描述符, 找到则成功.
  5. 否则查找失败.

若是未找到则返回NoSuchMethodError, 若为方法存在但是一个抽象类, 则抛出AbstractMethodError, 否则, 若方法存在, 但是没有访问权限, 则抛出IllegalAccessError.

查找成功, 虚拟机会标记这个entry为已解析, 并且在常量池中放入直接引用。

CONSTANT_InterfaceMethodRef_info解析

虚拟机首先检查class_index中的CONSTANT_Class_info项, 解析成功虚拟机在这个接口以及所有的父接口中查找指定的方法, 并检查访问权限。

  1. 如果被解析的类型是一个class, 抛出IncompatibleClassChangeError.
  2. 否则, 虚拟机检查被引用的接口中是否存在指定名称及描述符的接口, 若找到则成功。
  3. 否则虚拟机查找接口的所有父接口, 并递归查找, 找到则成功.
  4. 否则失败, 抛出NoSuchMethodError。

查找成功, 虚拟机会标记这个entry为已解析, 并且在常量池中放入直接引用。

CONSTANT_String_info解析

为了解析一个CONSTANT_String_info类型, 虚拟机需要在被解析的entry中放入一个interned string对象。 这个string对象必须拥有和string_index指向的CONSTANT_utf8_info中一样的字符串序列。

每一个JVM必须维护一个内部的string对象的列表. 如果一个string对象存在于这个string对象列表中, 则称它为interned.

若想intern一个COSTANT_String_info字符串序列, 虚拟机首先检查是否已存在于interned string列表中, 若已存在则返回已存在的引用, 否则虚拟机新建一个字符串序列存储这个string, 并把引用加入string列表中。 虚拟机然后把这个interned string的引用放入被解析的string的entry的data中, 至此解析完成。

在代码中可以通过String对象的intern()方法来把字符串放入常量池。

其他类型的解析

至于CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info都自己包含了值, 虚拟机可以直接使用它的值而不用作其他处理。

对于CONSTANT_Long_info以及CONSTANT_Double_info另一个有趣的地方在于他们占据了64个比特位, 也就8个字节, 两个constant pool entry。 其他的原始类型都不超过32bit, 占一个constant pool entry.

CONSTANT_Utf8-info以及CONSTANT_NameAndType_info类型不会被虚拟机指令直接引用, 只会出现在其他类型的常量池项目中, 在引用它的项目被解析时被解析。

直接引用

解析的最终目标是将符号引用替换为直接引用, 那么什么是直接引用?
类型, class变量以及class方法的直接引用可能就是方法区里的一个指针。 一个类型的直接引用, 可以是方法区中存有这个类型的数据结构的指针, 一个class变量的直接引用可能就是方法区的class变量的值的指针, 一个方法的直接引用可能就是指向方法区中的可以调用这个方法的数据结构的指针。

实例变量以及实例方法的直接引用使用偏移值来表示。 一个实例变量的直接引用可能就是从这个实例数据的开始的偏移值。 一个实例方法的直接引用也是方法表中的一个偏移值。

使用偏移值来表示实例变量以及方法的直接引用都要求有实例属性以及方法表有一个可预测的顺序。


https://www.artima.com/insidejvm/ed2/linkmodP.html

posted on 2018-12-10 01:13  浮舟z  阅读(363)  评论(0编辑  收藏  举报