10.1 Java类的链接

Java虚拟机运行时会在内部维护所有可用Java类的相关信息。虚拟机刚启动时,内部只包含Java核心类的相关信息。随着程序的运行,不断有新的Java类被加载到虚拟机中,变为可用状态。Java类被加载之后,经过链接和初始化就可以在虚拟机中使用了。链接的过程是把加载的Java类的字节代码中包含的信息与虚拟机的内部信息进行合并,使Java类的代码可以被执行。链接的过程由3个子步骤组成,分别是验证、准备和解析。在链接过程中,会对Java类的直接父类或父接口进行验证和准备,但是对类中形式引用的解析是可选的。

验证是用来确保Java类的字节代码表示在结构上是完全正确的。验证过程有可能会导致其他Java类或接口被加载。如果验证过程中发现字节代码的格式不正确,会抛出java.lang.VerifyError错误。通过Java编译器生成的字节代码通常不会出现验证错误。使用ASM等工具生成的字节代码可能会出现格式不正确的情况。

准备过程会创建Java类中的静态域,并将这些域的值设为默认值。在准备过程中并不会执行代码。准备过程中的一个重要环节是保证类加载时的类型安全。在链接过程中,可能有两个不同的类加载器对象同时开始加载一个Java类。在加载过程中,这两个类加载器对象也会分别加载Java类中的域和方法的参数和返回值引用的其他Java类。从类型安全的角度来说,不应该出现一个方法的参数类型对应的Java类,以及返回值类型对应的Java类由不同的类加载器对象来定义的情况。在准备过程中,当虚拟机中的某个类加载器对象开始加载某个类时,虚拟机会把该类加载器对象记录为该Java类的初始类加载器。记录完成后,虚拟机会马上进行一次检查。如果发现刚才的加载操作导致类型安全约束被破坏,则类加载过程不能进行。虚拟机会抛出java.lang.LinkageError错误。

解析过程是处理所加载的Java类中包含的形式引用。在一个Java类中会包含对其他类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。这些形式引用对应的Java类都被正确加载之后,当前Java类才能正常工作。在Java类中可能包含了对其他类中方法的调用,对于这些方法调用,在解析过程中需要检查所调用的方法确实存在。

在解析过程中会遇到的一个问题是如何处理复杂的引用关系图。Java类之间的引用关系可能非常复杂。在解析一个Java类的过程中,可能导致其他的Java类被加载和解析,从而导致更多的Java类被加载和解析。通常可以利用两种策略来处理这种情况:一种是提前解析,即在链接时递归地对依赖的所有形式引用都进行解析,这种做法的缺点是性能比较差;另外一种策略是延迟解析,即只在真正需要一个形式引用时才进行解析,也就是说,如果一个Java类只是被引用,没有在程序运行中被真正用到,这个类就不会被解析。这种做法解决了提前解析方式性能较差的问题。不同的虚拟机实现可能采取不同的策略,不管采用哪种策略,都不会对程序的正确性造成影响。

代码清单10-1中给出了一个测试虚拟机的类解析策略的Java程序。类LazyLink中引用了类ToBeLinked,但是没有创建ToBeLinked类的对象或引用类的静态域或方法。在Java SE 7的OpenJDK实现中,先把ToBeLinked类的字节代码删掉,再运行LazyLink类,程序并没有抛出错误,而是可以正确运行。这说明OpenJDK采用了延迟解析的策略。虽然LazyLink类引用了ToBeLinked类,但是在最开始的时候,ToBeLinked类只是作为一个形式引用存在。在LazyLink类的运行过程中并没有实际用到ToBeLinked类,因此ToBeLinked类不会被加载和解析。所以即便ToBeLinked类的字节代码不存在,程序运行也不会出现错误。

代码清单10-1 测试虚拟机的类解析策略的示例程序


public class LazyLink{

public static void main(String[]args){

ToBeLinked toBeLinked=null;

System.out.println("使用延迟解析。");

}

}


如果把代码清单10-1中的“ToBeLinked toBeLinked=null;”改成“ToBeLinked toBe-Linked=new ToBeLinked();”,再按照相同的方式运行,则会抛出异常。这是由于程序运行中需要创建ToBeLinked类的对象,因此需要把ToBeLinked类加载到虚拟机中并进行链接。