Java虚拟机之连接模型

       Java程序在运行之前,每个类和接口都是独立的class文件。JVM是怎样装载和解析这些class文件,使它们之间能够相互关联呢?下面我们来深入研究Java体系结构中非常重要的一方面——连接模型。
       Java程序经过编译后,得到的是每个类或者接口的独立的class文件。虽然这些文件看上去毫无关联,但是JVM通过动态连接过程,使它们之间通过接口(harbor)符号相互联系,或与Java API的class文件联系。
       Class文件把它所有的引用符号保存在一个地方——常量池。每一个class文件有一个常量池,每一个被Java虚拟机装载的类或者接口都有一份内部版本的常量池,被称作运行时常量池。运行时常量池是一个特定于实现的数据结构,数据结构映射到class文件中的常量池。因此当一个类型被首次装载时,所有来自于类型的符号引用都装载到了类型的运行时常量池。
       当程序运行到某个时刻,如果某个特定的符号引用将要被使用,它首先要被解析。解析过程就是根据符号引用查找到实体,再把符号引用替换成一个直接引用的过程。因为所有的符号引用都保存在常量池中,所以这个过程常被称作常量池解析。来自相同或不同方法中的几条指令,可能指向同一个常量池入口,但是每一个常量池入口都只被解析一次。当符号引用被一条指令解析过后,来自其他指令的访问该符号引用,都使用第一次解析出的直接引用结果。

       不同的Java虚拟机实现允许在程序执行的不同时间进行解析,主要有两种方式:
       早解析:从初始类开始,到后续的各个类,直到所有的符号引用都被解析
       迟解析:只会在执行程序第一次用到这个符号引用的时候才去解析

1.动态扩展

       动态扩展:程序运行过程中,通过传递类型的名字到java.lang.Class的forName()方法,或者用户自定义的类装载器的loadClass()方法,临时决定装载和使用的类型。
       动态扩展的两种方式:

  • 使用java.lang.Class的forName()方法

      public static Class<?> forName(String className)
                        throws ClassNotFoundException
      public static Class<?> forName(String name,
                               boolean initialize,
                               ClassLoader loader)
                        throws ClassNotFoundException
       String类型的className参数表示装载类型的全限定名;boolean类型的initialize参数表示是否在forName()方法返回前连接并初始化;ClassLoader类型的loader参数表示用户定制的类装载器的引用,如果用默认的启动类装载器,只需传递null。

  • 使用用户自定义类装载器的loadClass()方法

      public Class<?> loadClass(String name)
                   throws ClassNotFoundException
      protected Class<?> loadClass(String name,
                             boolean resolve)
                      throws ClassNotFoundException
      String类型的name参数表示装载类型的全限定名;boolean类型的resolve参数表示是否在装载时执行该类型的连接。

2.常量池解析

1. 解析CONSTANT_Class_info入口

       这种入口类型用来表示指向类(包括数组类)和接口的符号引用。

  • 数组类

       指向数组类的符号引用最终被解析为一个Class实例。
       如果数组的元素类型是引用类型,虚拟机用当前类装载器解析元素类型。如果数组的元素类型是基本类型,那么虚拟机立即创建关于那个元素类型的新数组类,维数也在此时确定,然后创建一个Class的实例来代表这个类型。如果是关于引用的数组,数组会标记为是由定义它的元素类型的类装载器定义的。如果是关于基本类型的数组,数组类会被标记为是由启动类装载器定义的。

  • 非数组类和接口

      要解析任何指向非数组类和接口的符号引用,需要执行如下步骤:

步骤1:装载类型

      步骤1a:装载类型或者任何超类型
      虚拟机必须确定是否被引用的类型已经被装载进了当前命名空间。对于每一个类装载器,Java虚拟机维护一张列表,其中记录了所有其装载的类型的名字。每张这样的列表,就是JVM内部的命名空间,能够每个类装载器都只装载一次给定名字的类型。
      如果引用的类型是一个类,并且不是java.lang.Object,JVM会装载它的超类,一直重复到超类为Object为止。在从Object返回的路上,JVM装载每个类型直接实现的任何接口。在装载接口的时候,JVM会装载它们直接扩展的任何其他接口。这样就可以确保符号引用的类型以及该类型的超类和超接口都被装载了。
     步骤1b:访问权限检查
     如果发起引用的类型没有访问被引用的类型的权限,JVM抛出IllegalAccessError异常。

步骤2:连接并初始化类型和任何超类

     JVM解析某类型(不是接口)符号引用之前,必须确认它的所有超类都已经被初始化。超类必须在子类之前被初始化。如果一个类型还没有被连接,在初始化之前必须被连接。只有超类必须被初始化,超接口是不必的。
     步骤2a:校验类型
     校验阶段可能要求虚拟机装载新的类型来确认字节码符合java语言的语义。其他类可能被装载,甚至被连接,但是肯定不会被初始化。如果在校验阶段JVM出现问题,会抛出VerifyError异常。
     步骤2b:准备类型
     JVM为类变量以及随实现不同而有差别的数据结构分配内存。
      步骤2c:可选的步骤,解析类型
      关于被引用的类型,步骤1a、2a、2b已经解析了发起引用的类型的常量池的CONSANT_Class_info入口。步骤2c是关于被引用类型(而非发起引用的类型)中所包含的符号引用的解析。
      步骤2d:初始化类型
      初始化包括两个步骤:如果类型拥有任何超类,初始化类型的超类是按照自顶向下的顺序进行的;如果类型拥有一个类初始化方法,那也在此时执行。

2. 解析CONSTANT_Fieldref_info入口

       要解析类型是CONSTANT_Fieldref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM按照如下步骤执行字段搜索过程:
      1)虚拟机在被引用的类型中查找具有指定的名字和类型的字段。如果虚拟机找到了这样一个字段,这个字段就是成功的字段搜索结果。
       2)否则,虚拟机检查类型直接实现或扩展的接口,以及递归地检查它们的接口。如果找到名字和类型都符合的字段,这个字段就是成功的字段搜索结果。
       3)否则,如果类型拥有一个直接的超类,虚拟机检查类型的直接超类,并且递归地检查类型的所有超类。如果找到了名字和类型都符合的字段,这个字段就是成功的字段搜索结果。
       4)否则,字段搜索失败。
       如果字段搜索失败,JVM抛出NoSuchFieldError异常;如果字段搜索成功,但是当前类没有权限去访问该字段,JVM抛出IllegalAccessError异常。如果字段搜索到并且有访问权限,JVM把这个入口标记为已解析,并在这个常量池入口的数据中放上指向这个字段的直接引用。

3. 解析CONSTANT_Methodref_info入口

      要解析类型是CONSTANT_Methodref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM使用如下步骤执行方法解析:
      1)如果被解析的类型是一个接口,而非类,JVM抛出IncompatibleClassChangeError异常
      2)否则,被解析的类型是一个类。JVM检查被引用的类是否有一个方法符合指定的名字以及描述符。如果JVM找到了这样的一个方法,这个方法就是成功的方法搜索结果。
      3)否则,如果类有一个直接的超类,JVM检查类型的直接超类,并且递归地检查类的所有超类,查看是否有方法符合指定的名字以及描述符。如果找到了这样的一个方法,这个方法就是成功的方法搜索结果。
      4)否则,JVM检查是否这个类直接实现了任何接口,并且递归地检查由类型直接实现的接口的超接口。查找是否有方法符合指定的名字以及描述符,如果找到了这样的一个方法,这个方法就是成功的方法搜索结果。
      5)否则,方法搜索失败。
       如果JVM没有找到名字、返回类型、参数数量和类型都符合的方法,JVM抛出NoSuchMethodError异常。如果方法存在,但是方法是一个抽象方法,JVM抛出AbstractMethodError异常。如果方法存在,但是当前类没有访问权限,JVM抛出IllegalAccessError异常。否则,JVM将这个常量池入口标记为已解析,并在数据中放上指向该方法的直接引用。

4. 解析CONSTANT_InterfaceMethodref_info入口

       要解析类型是CONSTANT_InterfaceMethodref_info的常量池入口,虚拟机必须首先解析class_index项中指明的CONSTANT_Class_info入口。如果解析CONSTANT_Class_info成功完成,JVM按照如下步骤来执行接口方法解析:
     1)如果被解析的类型是一个类,而非接口,JVM抛出IncompatibleClassChangeError异常。
     2)否则,被解析的类型是一个接口。JVM检查被引用的接口是否有方法符合指定的名字和描述符。如果发现了这样的一个方法,该方法就是成功的接口方法搜索结果。
     3)否则,JVM检查接口的直接超接口,并且递归的检查接口的所有超接口以及java.lang.Object类来查找符合指定名字和描述符的方法。如果发现了这样的一个方法,该方法就是成功的接口方法搜索结果。
     4)如果JVM没有在被引用的接口和它的任何超类中找到名字、返回类型、参数的数量和类型都符合的方法,JVM抛出NoSuchMethodError异常。
     5)否则,JVM将这个常量池入口标记为已解析,并在数据中放上指向该方法的直接引用。

5. 解析CONSTANT_String_info入口

      要解析类型是CONSTANT_String_info的入口,JVM必须把一个指向字符串对象的引用放置到要被解析的常量池入口数据中。该字符串对象(java.lang.String类的实例)必须按照string_index项在CONSTANT_String_info中指明的CONSTANT_Utf8_info入口所指定的字符顺序组织。
      每一个JVM必须维护一张内部列表,它列出了所有在运行程序的过程中已被“拘留(intern)”的字符串对象的引用。维护这个列表的关键是任何特定的字符序列在这个列表上只出现一次。在Java程序中,可以调用String类的intern()方法来拘留一个字符串。
      要拘留CONSTANT_String_info入口所代表的字符序列,JVM要检查内部拘留名单上这个字符序列是否已经在编了。如果已经在编,JVM使用指向以前拘留的字符串对象的引用。否则JVM按照这个字符序列创建一个新的字符对象,并把这个对象的引用编入列表。

6. 解析其他类型的入口
       CONSTANT_Integer_info、CONSTANT_Long _info、CONSTANT_Float _info和CONSTANT_Double _info入口本身包含它们所表示的常量值,它们可以直接被解析。 CONSTANT_Utf8_info和CONSTANT_NameAndType_info类型的入口永远不会被指令直接引用。它们只有通过其他入口类型才能被引用,并且在那些引用入口被解析时才被解析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值