zhaoyu@home:~$

jvm-加载、链接、初始化

JVM动态地加载,链接和初始化class文件。加载是使用名称找到class文件的二进制表示并创建一个类或者接口。链接是将一个类或者接口和JVM运行时状态 结合起来,用于进行执行。初始化一个类或者接口是执行类或者接口的初始化方法<clinit>

运行时常量池

JVM为每种类型维护了一个常量池,是一种服务于编程语言的符号表的运行时数据结构。
一个类或者接口的二进制字节码中的constant_pool表在类或者接口被创建时,用于构成运行时常量池。运行时常量池中的所有引用最初都是符号化的。运行时 常量池中继承自类或者接口二进制字节码的符号引用如下:

  • 字节码中CONSTANT_Class_info结构的符号引用。这个引用通过Class.getName获取类或者接口的名称。
    • 对于非数组的类或者接口,名称是类或者接口的二进制名称。
    • 对于n维数组,返回类似[[[D
  • 字节码中CONSTANT_Fieldref_info的符号引用,表示字段。
  • 字节码中CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info,CONSTANT_MethodHandle_info,CONSTANT_MethodType_info 等的符号引用。
  • 字节码中CONSTANT_InvokeDynamic_info的符号引用,这个引用给出:
    • 一个method handle的引用,用于invokedynamic指令的bootstrap方法。
    • 一系列符号引用(包括类,methodType,methodHandler),字符串,运行时常量,用于bootstrap方法的静态参数。
    • 一个方法名称和方法描述。

一些运行时值则不是从字节码的constant_pool表中继承的符号引用,如下:

  • String类的字符串文本,继承自字节码的CONSTANT_String_info结构。java要求相等的字符串引用相同的String实例。String.intern方法调用 是对同一个类实例的引用。因此,如下的表示必须为true:
      ("a" + "b" + "c").intern() == "abc"
    
  • 继承自字节码的CONSTANT_Integer_info, CONSTANT_Float_info, CONSTANT_Long_info, 或 CONSTANT_Double_info的运行时常量值。

字节码constant_pool表中剩余的结构,CONSTANT_NameAndType_info和CONSTANT_Utf8_info只在生成符号引用时间接使用,不会在运行时常量池中。

JVM启动

JVM通过使用bootstrap类加载器创建一个初始化类启动,这个类根据JVM实现方式指定。然后JVM链接这个初始化类。初始化并调用main方法。main方法 会引起其他类或接口的链接,方法调用。
在JVM的实现中,初始化类可以通过命令行参数提供,也可以提供一个设置了classloader的初始化类,这个classloader再加载整个程序。

创建和加载

一个类或者接口C的创建是由另一个在运行时常量池中引用了C的类或者接口D触发的。也可能是由D调用了JavaSE类库引起的,例如反射。

如果C不是一个数组,那么它通过使用ClassLoader加载二进制文件创建。数组没有二进制表示,由JVM创建。

有两种ClassLoader。一种由JVM提供的bootstrap ClassLoader。另一种是由用户定义的classLoader。每个用户定义的ClassLoader 是抽象类ClassLoader子类的实例。应用使用用户定义的类加载器扩展JVM的动态加载。用户定义的类加载器用于加载用户自定义的源码,如: 通过网络下载,实时生成,或从一个加密文件中抽取生成。

一个类加载器L可以直接定义并创建类C,也可以委托给另一个类加载器。如果L直接创建C,我们称为L定义了C,或者L是C的定义加载器。 当委托给另一个类加载器时,初始化的Loader和加载、定义的类的Loader不同。如果类加载器L创建了C,不管是直接定义还是委托,我们称L初始化了C, 或L是C的初始化加载器。

在运行时,一个类或者接口不是由它的name决定的。而是由二进制name和ClassLoader对共同决定的。 JVM使用下面三种途径来加载和创建C:

  • 如果C不是数组,且D引用了C:
    • 如果D被bootstrap类加载器定义,D引用了C,那么bootstrap类加载器初始化C。
    • 如果D被用户定义的类加载器定义,那么这个用户定义的类加载器初始化C。
  • 否则,C是一个数组,数组的创建由JVM直接负责,同时D的定义加载器在创建C的过程中被使用。

如果在加载类过程中发生异常,linkageError的子类实例会被抛出。

如果JVM在加载类C的过程中,类加载器抛出了异常ClassNotFoundException,JVM必须封装为一个NoClassDefFoundError抛出。

一个正常的ClassLoader应该满足下面3个条件:

  • 给出一个相同的name。一个classloader应该返回相同的class对象。
  • 如果L1将C的加载委托给了L2,那么对于C的直接超类或直接父接口T,C类型的字段,C类型的方法参数,C作为方法的返回类型,L1和L2 必须返回相同的class对象。
  • 如果一个用户自定义的类加载器要预取类或者接口的二进制表示,或一起加载一组相关的类。那么它必须在还没有预期或者组加载的时候 反应出加载错误。

bootstrap 类加载器

bootstrap 类加载器加载一个非数组类C的过程如下:

  1. JVM查看bootstrap类加载器是否已经被记录为C的初始化加载器。如果是,那么说明C已经被创建,不需要创建。
  2. 否则,JVM调用bootstrap加载器的方法查找C的二进制表示,通常这种表示是文件系统中的一个文件。如果没有找到,那么会 抛出ClassNotFoundException。
  3. 然后,JVM使用bootstrap加载器从文件表示中生成一个类。

用户类加载器

加载一个非数组类C的过程如下:

  1. JVM查看用户类加载器L是否已经被记录为C的初始化加载器。如果是,那么说明C已经被创建,不需要创建。
  2. 否则,JVM调用loadClass(N)方法,方法的返回值是一个被创建的类或接口C。JVM将L记录为C的初始化loader。
    当loadClass方法被调用,L必须执行如下两个步骤之一,来加载C。
    1. L可以创建一个byte数组将C表示为ClassFile结构的数组;然后必须调用ClassLoader的defineClass方法,该方法会让 JVM从byte数组中通过算法生成一个类C的表示。
    2. L可以将C的加载委托给L2,可以通过直接或间接地调用L2的loadClass方法,调用后会生成类C。
      如果L不能加载类C,那么异常ClassNotFoundException被抛出。

数组类的创建

类加载器L创建数组类C的步骤如下,L既可以是bootstrap也可以是用户定义的。
如果L已经被记录为元素为C的数组类的类初始加载器,那么不会创建数组类。否则执行如下步骤:

  1. 如果数组的元素类型为引用类型,L递归地调用对象加载的步骤,完成数组类型C的加载和创建。
  2. 接着JVM使用元素类型和数组的维度创建一个新的数组类。

加载限制

JVM加载类时,有可能会出现不同的类加载器加载了同一个类C,相同的名称在不同的类加载器中却是不同的类对象。 当字段和方法中使用到某个类型时,让不同类加载器L1和L2加载相同名称的类指向同一个类对象非常重要。如果用户自己写了一个称 为”java.lang.Object”的类,并在另一个类加载器中加载,那么系统将产生多个不同的Object类,可想而知,程序将一片混乱。

为了确保这一点,JVM在准备阶段和处理阶段采用了N(L1)=N(L2)的加载约束,即L1加载的N和L2加载的N是相同的。为实现这个约束,JVM会在 某些特定的时间,记录某个类加载器是某个类的初始化加载器,在记录后,JVM必须立即检查是否有加载约束被违反,如果违反,则撤销记录。并 抛出LinkageError,加载操作反生失败。

从class文件生成一个类

步骤如下:

  1. JVM检查是否已经记录了L作为N的初始化加载器,如果已经加载,则抛出LinkageError。
  2. 否则,JVM尝试解析文件:
    • 如果文件不是ClassFile结构,则抛出ClassFormatError。
    • 如果主从版本不支持,则抛出UnsupportedClassVersionError。
    • 如果文件不能表示名称为N的类,抛出NoClassDefFoundError或它的子类。
  3. 如果类C有直接父类,从类C到直接父类的符号引用需要处理。如果C是一个接口,必须让Object作为它的直接父类。在这个阶段会有如下异常发生:
    • 如果C的直接父类不是类而是接口,会抛出IncompatibleClassChangeError。
    • 否则,如果C的任何父类是C本身,抛出ClassCircularityError。
  4. 如果C有直接父接口,从C到直接父接口的引用需要处理。
    • 如果C的直接父接口是类,抛出IncompatibleClassChangeError。
    • 否则,如果C的任何父类是C本身,抛出ClassCircularityError。
  5. Java虚拟机将C标记为使用L作为其定义类加载器,并记录L是C的初始加载器。

链接

链接一个类或者接口涉及到验证和准备这个类或接口,它的直接父类,它的直接父接口,它的元素类型(如果是数组)。符号引用的处理是链接的一部分。

链接前需要满足如下条件:

  • 链接前,类或接口需要加载完整。
  • 链接前,类或接口需要验证和准备完成。
  • 链接过程中检测到的错误会在程序中的某个点抛出,此时程序可能会直接或间接地执行某些操作链接到错误相关的类或接口。

验证

验证确保二进制表示的结构正确。可能会引发额外的类或者接口被加载,但是不会引发他们的验证和准备。
如果二进制表示不满足静态或者机构化的约束。VerifyError会被抛出。

准备

准备包括创建类或者接口的静态字段,并初始化这些字段的默认值。这不需要执行任何JVM代码;显示的静态字段初始化是初始化的一部分,而不是准备阶段的。

在准备阶段,JVM也实行了加载限制。准备可能发生在对象创建和初始化之间的任何时候。

解析

JVM指令anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, 和 putstatic 会让符号引用指向运行时常量池。执行 任意上述指令需要解析它们的符号引用。

解析是在运行时常量池中将符号引用动态转换为直接引用(指针,偏移量等)的过程。

一次invokedynamic指令的符号引用的解析并不表示,相同符号引用对于其他invokedynamic指令也已经解析完成了。但是对于其他非invokedynamic指令, 一次指令的符号解析意味着相同符号引用已经被解析过了。

如果解析的过程中发生错误,那么在用到该符号引用时,IncompatibleClassChangeError或其子类会被抛出。

解析一个类或接口D的运行时常量池中符号引用包括了:类或者接口解析;字段解析;方法解析;接口方法解析;方法类型和方法handle解析;调用掉描述符 解析。具体解析过程参考JVM的官方文档。

初始化

实例化即执行一个类或者接口的实例化方法。

类或者接口C只能在以下情况下被初始化:

  • 任何一个引用了C的JVM指令new,getstatic,putstatic或invokestatic的执行。
  • 首次调用java.lang.invoke.MethodHandle实例。
  • 在类库中调用一些反射方法,如在Class类或java.lang.reflect包中。
  • 如果C是类,它的其中一个子类初始化了。
  • 如果C是接口,声明了一个非抽象非静态方法,直接或间接实现了C的类实例化了。
  • 如果C是一个类,它设置为JVM启动的初始化类。

由于JVM是多线程的,初始化需要小心处理同步。一个类或接口的初始化也有可能递归地依赖于自身。JVM需要同时处理同步和递归问题。