Chapter 12. Execution

此章节规定了程序执行期间发生的活动。他围绕 Java 虚拟机以及构成程序的类、接口和对象的声明周期进行组织编写。

Java 虚拟机通过加载指定的类然后调用该类中的 main 方法来启动。第 12.1 节概述了执行 main 所涉及的加载、链接和初始化步骤,作为本章节的概念的介绍。下一个部分讲述了加载 12.2 、链接 12.3 和初始化 12.4 的细节。

本章后续部分说明创建新类实例的过程(第 12.5 节 );和类实例的最终确定( 12.6 )。它通过描述类的卸载(第 12.7 节 )和程序退出时遵循的过程(第 12.8 节 )来结束。

12.1 Java Virtual Machine Startup

Java 虚拟机通过调用某个指定类的 main 方法开始执行,并传递给它一个参数,该参数是一个字符串数组。在本规范的示例中,这个特定类通常称为 Test

Java 虚拟机启动的精确语义在 Java Virtual Machine Specification, Java SE 8 Edition 的第 5 章中给出。在这里,我们从 Java 编程语言的角度概述了该过程。

将初始类指定给 Java 虚拟的方式超出了本规范的范围,但在使用命令行的主机环境中,这是典型的,对于作为命令行参数指定的类的全限定名,以及将后面的命令参数作为字符串提供给方法 main

例如,在 UNIX 实现中,命令行:

java Test reboot Bob Dot Enzo

通常会通过调用类 Test(未命名包中的类)的方法 main 类启动 Java 虚拟机,并向其传递包含四个字符串 "reboot"、"Bob"、"Dot" 和 "Enzo" 的数组。

我们现在概述 Java 虚拟机执行 Test 可能采取的步骤,作为加载、连接和初始化过程的例子,这些过程将在后面的部分中进一步描述。

12.1.1 Load the Class Test

最初尝试执行类 Testmain 方法时,发现没有加载类 Test —— 也就是说,Java 虚拟机当前不包含这个类的二进制表示。然后,Java 虚拟机使用类加载器来尝试找到这样的二进制表示。如果这个过程失败,就会抛出一个错误。该加载张将在 12.2 中进一步描述。

加载 Test 后,必须调用 main 之前对其进行初始化。和所有(类或接口)类型一样,Test 在初始化之前必须被连接。连接包括验证准备(可选)解析 。链接将在 12.3 中进一步描述。

验证会检查 Test 的加载表示是否格式良好,是否有正确的符号表。验证还检查实现 Test 的代码是否符合 Java 编程语言和 Java 虚拟机的语义要求。如果在验证过程中检测到问题,就会抛出一个错误。12.3.1 中进一步描述了验证。

准备包括静态存储和 Java 虚拟机实现内部使用的任何数据结构的分配,比如方法表。12.3.2 中进一步描述了准备工作。

解析是检查从 Test 到其他类和接口的符号引用的过程,通过加载提到的其他类和接口并检查引用使用正常。

在初始化连接时,解析步骤是可选的。一种实现可以从很早就被链接的类或接口中解析符号引用,甚至可以从被引用的类和接口中进一步递归解析所有符号引用。(该解决方案可能会导致这些进一步加载和链接步骤的错误。)这种实现选择代表了一个极端,类似于在 C 语言的简单实现中已经做了多年的这种“静态”链接。。(在这些实现中,编译后的程序通常被表示为 “a.out” 文件,改文件包含该程序的完全链接版本,包括到该程序所使用的程序例程的完全解析的连接。这些库例程的副本包含在 “a.out” 文件中。)

一种实现可以选择仅在符号引用被主动使用时解析它;对所有符号引用一致使用这种策略将代表的是“最懒惰”的解决方式。在这种情况下,如果 Test 有几个对另一个类的符号引用,那么这些引用可能会在使用时一次解析一个,或者如果这些引用的程序执行期间从未使用过,则可能根本不解析。

对何时执行解析的唯一要求是,在解析过程中检测到的任何错误都必须在程序中的某个点抛出,在该点上,程序将采用一些可能直接或间接地采取一些操作,这些操作可能需要链接到设计错误的类或接口。使用上面描述的 “静态” 示例实现选择,如果加载和链接错误涉及类 Test 或任何进一步递归引用的类和接口中提到的类和接口,那么它们可能在程序执行之前发生。在实现“最懒惰”解析的系统中,只有在积极使用不正确的符号引用时才会抛出这些错误。

解析过程在第 12.3.3 节中进一步描述。

12.1.3 Initialize Test: Execute Initializers

在我们接下来的示例中,Java 虚拟机仍在尝试执行 Test 类的 main 方法。仅当类已初始化时才允许这样做(第 12.4.1 节 )。

初始化包括按文本顺序执行类 Test 的任何类变量初始化程序和静态初始化程序。但是在初始化 Test 之前,它的直接超类必须被初始化,以及它的直接超类的直接超类,以此递归类推。在最简单的情况下,TestObject 作为其隐式直接超类;如果类 Object 尚未初始化,则必须初始化 Test 之前对齐进行初始化。类 Object 没有超类,所以递归在这里终止。

如果类 Test 有另一个类 Super 作为它的父类,那么 Super 必须在 Test 之前初始化。这需要加载、验证和准备 Super (如果还没有这样做的话),根据实现的不同,可能涉及递归地解析来自 Super 的符号引用等等。

因此,初始化可能会导致加载、连接和初始化错误,包括涉及其他类型的此类错误。

初始化过程在第 12.4 节中进一步描述。

12.1.4 Invoke Test.main

最后,在 Test 类的初始化完成后(在此期间可能发生了其他相应的加载、连接和初始化),调用 Testmain 方法。

方法 main 必须声明为 publicstaticvoid。它必须指定一个声明类型为字符串数组 的形式参数(第 8.4.1 节)。因此,可以接受以下任一声明:

public static void main(String[] args)
public static void main(String... args)

12.2 Loading of Classes and Interfaces

加载 指的是找到具有特定名称的类和接口类型的二进制形式的过程,可能是通过即时计算,但更常见的是通过检索 Java 编译器先前从源代码计算的二进制表示,并根据该二进制形式构造表示类或接口的 Class 对象。

*Java Virtual Machine Specification, Java SE 8 Edition * 的第 5 章给出了加载的精确语义。在这里,我们从 Java 编程语言的角度概述了该过程。

类或接口的二进制格式通常是上面引用的 *Java Virtual Machine Specification, Java SE 8 Edition * 中描述的类文件格式,但是其他格式也是可能的,只要它们满足 13.1 中指定的要求。ClassLoader 类的 defineClass 方法可用于从 class 文件格式的二进制表示中构造 Class 对象。

行为良好的类加载器维护以下属性:

  • 给定相同的名称,一个好的类加载器总是返回相同的类对象。
  • 如果一个类加载器 L1 将类 C 的加载委托给另一个加载器 L2,那么对于 C 的直接超类或直接超接口,或者作为 C 中的字段类型,或者作为 C 中的方法或构造函数的形参类型,或者作为 C 中方法的返回类型出现的任何 T 类型,L1L2 应该返回相同的 Class 对象。

恶意的类加载器可能会破坏这些属性。然而,它不能破坏类型系统的安全性,因为 Java 虚拟机防止了这一点。

进一步讨论这些问题,请参阅 Java Virtual Machine Specification, Java SE 8 Edition and the paper Dynamic Class Loading in the Java Virtual Machine, by Sheng Liang and Gilad Bracha, in Proceedings of OOPSLA '98, published as ACM SIGPLAN Notices, Volume 33, Number 10, October 1998, pages 36-44 。Java 编程语言设计的一个基本原则是,运行时类型系统不能被用 Java 编程语言编写的代码破坏,甚至不能被诸如 ClassLoaderSecurityManager 之类的敏感系统类的实现破坏。

12.2.1 The Loading Process

加载过程由 ClassLoader 类及其子类实现。

ClassLoader 的不同子类可以实现不同的加载策略。特别是,类加载器可以缓存类和接口的二进制表示,根据预期的使用情况预读取它们,或者一起加载一组相关的类。这些活动对于正在运行的应用程序可能不是完全透明的,例如,如果因为类加载器缓存了旧版本而找不到类的新编译版本。然而,类加载器的责任是旨在程序中没有预读取或分组加载的地方反映加载错误。

如果在类加载过程中出现错误,则在程序中(直接或间接)使用该类型的任何点都会引发 LinkageError 类的子类之一的实例:

  • ClassCircularityError:无法加载类或接口,因为它将是其自己的超类或超接口(第 8.1.4 节、第 9.1.3 节、第 13.4.4 节)。
  • ClassFormatError:声明指定所请求的编译类或接口的二进制数据格式不正确。
  • NoClassDefFoundError:相关的类加载器找不到请求的类或接口的定义。

因为加载涉及新数据结构的分配,所以它可能会因 OutOfMemoryError 而失败。

12.3 Linking of Classes and Interfaces

链接 是获取类或接口类型的二进制形式,并将其组合到 Java 虚拟机的运行时状态中,以便可以执行的过程。类或接口类型总是在连接之前加载。

链接设计三种不同的活动:验证、准备和解析符号引用。

Java Virtual Machine Specification, Java SE 8 Edition 的第 5 章中给出了链接的精确语义。在这里,我们从 Java 编程语言的角度概述了这个过程。

如果考虑到 Java 编程语言的语义,类或接口在初始化之前就被完全验证和准备,并且在链接期间检测到的错误在程序中的某个点被抛出,在该点处程序采取了可能需要链接到错误中涉及的类或接口的一些动作,则该规范允许关于链接活动(以及由于递归,加载)何时发生的实现灵活性。

例如,一个实现可以选择仅当一个类或接口被使用时(惰性或延迟解析),单独解析它当的每个符号引用,或者在类被验证时一次性解析它们(静态解析)。这意味着,在一些实现中,在类或接口被初始化之后,解析过程可以继续。

因为链接涉及新数据结构的分配,所以它可能会因 OutOfMemoryError 而失败。

12.3.1 Verification of the Binary Representation

验证确保类或接口的二进制表示在接口上是正确的。例如,它检查每条指令都有一个有效的操作码;每个分支指令都分支到其他指令的开始,而不是一条指令的中间;每个方法都具有结构正确的签名;并且每条指令都遵守 Java 虚拟机语言的类型规则。

如果在验证过程中出现错误,那么将在程序中导致该类在被验证的点处抛出 LinkageError 类的以下子类的实例:

  • VerifyError:类或接口的二进制定义未能通过一组必须的检查,以验证它符合 Java 虚拟机语言的语义,并且不会破坏 Java 虚拟机的完整性。(一些示例见 13.4.2、13.4.4、13.4.9 和 13.4.17。 )

12.3.2 Preparation of a Class or Interface Type

准备工作包括为类或接口创建 static 字段(类变量和常量),并将这些字段初始化为默认值(4.12.5)。这不需要执行任何源代码;静态字段的显式初始化器作为初始化(12.4)的一部分执行,而不是准备。

Java 虚拟机的实现可以在准备时预先计算额外的数据结构,以便使以后对类或接口的操作更有效。一种特别有用的数据结构是“方法表”或其他数据结构,它允许在一个类的实例上调用任何方法,而不需要在调用时搜索超类。

12.3.3 Resolution of Symbolic References

类或接口的二进制表示引用其他类和接口(13.1)的二进制名称(13.1),象征性地引用其他类和接口及其字段、方法和构造函数。对于字段和方法,这些符号引用包括字段或方法所属的类或接口类型的名称,以及字段或方法本身的名称,以及适当的类型信息。

在符号引用可以被使用之前,它必须经过解析,其中符号引用被检查为正确的,并且通常被替换为直接引用,如果引用被重复使用,则直接引用可以被更有效地处理。

如果在解析过程中出现错误,那么将会抛出一个错误。最典型的情况是,这将是 IncompatibleClassChangeError 类的下列子类之一的实例,但也可能是 IncompatibleClassChangeError 的其他子类的实例,甚至是 IncompatibleClassChangeError 类本身的实例。此错误可能在程序中直接或间接使用对该类型的符号引用的任何位置引发:

  • IllegalAccessError:遇到了指定字段的使用或赋值、方法的调用或类实例的创建的符号引用,而包含该引用的代码无权访问这些引用,因为该字段 privateprotectedpackage 访问权限(非 public)声明的,或者因为该类未声明为 public
    例如,如果一个最初声明为 public 的字段在另一个引用该字段的类被编译后被更改为 private,就会发生这种情况(13.4.7)。
  • InstantiationError:遇到了在类创建表达式中使用的符号引用,但无法创建实例,因为该引用引用了接口或抽象类。
    例如,如果一个原本不是抽象的类在另一个引用该类的类被编译后变成了抽象的,就会发生这种情况(13.4.1)。
  • NoSuchFieldError:遇到了引用特定类或接口的特定字段的符号引用,但该类或接口不包含该名称的字段。
    例如,如果在编译了引用某个字段的另一个类之后,从该类中删除了该字段声明,就会出现这种情况(13.4.8)。
  • NoSuchMethodError:遇到了引用特定类或接口的特定方法的符号引用,但该类或接口不包含该签名的方法。
    例如,如果在编译了引用某个方法的另一个类之后,从该类中删除了该方法声明,就会出现这种情况(13.4.12)。

此外,如果某个类声明了一个无法找到实现的 native 方法,则可能会引发 UnsatisfiedLinkErrorLinkageError的子类。根据 Java 虚拟机(12.3)的实现所使用的解析策略的类型,如果使用了方法,或者更早,就会出现错误。

12.4 Initialization of Classes and Interfaces

初始化类包括执行它的静态初始化器和在类中声明的 static 字段(类变量)的初始化器。

初始化接口包括为接口中声明的字段(常量)执行初始化器。

12.4.1 When Initialization Occurs

类或接口类型 T 将在第一次出现以下任何一种情况之前立即初始化:

  • T 是一个类并且创建了 T 的一个实例。
  • 调用由 T 声明的 static 方法。
  • 分配一个由 T 声明的 static 字段。
  • 使用由 T 声明的 static 字段,并且该字段不是常量变量(4.12.4)。
  • T 是顶级类(7.6),并且执行了断言语句(14.10),它在词法上嵌套在 T (8.1.3)中。

当一个类被初始化时,它的超类(如果它们之前没有被初始化),以及声明任何默认方法的超接口(8.1.5)(如果它们之前没有被初始化)也会被初始化。接口的初始化本身不会导致它的任何超接口的初始化。

引用 static 字段(8.3.1.1)只会初始化实际声明静态字段的类或接口,即使它可能通过子类,子接口或实现接口的类的名称被引用。

在类 Class 和包 java.lang.reflect 中调用某些反射方法也会导致类或接口初始化。

类或接口在任何其他情况下都不会初始化。

注意,编译器可以在接口中生成合成的默认方法,也就是说,既没有显示声明也没有隐式声明的默认方法(13.1)。这些方法将触发接口的初始化,尽管源代码没有给出应该初始化接口的指示。

目的是一个类或接口类型有一组初始化器,使它处于一致的状态,并且这个状态是其他类观察到的第一个状态。静态初始值设定项和类变量初始值设定项是按文本顺序执行的,可能不会引用在勒种声明的类变量,这些类变量的声明在使用后以文本形式出现,即使这些类变量在作用域内(8.3.3)。这种限制旨在编译时检测大多数循环或错误的初始化。

初始化代码是不受限制的,这一事实允许构造这样的例子,其中在评估初始化表达式之前,当类变量仍然具有其初始默认值时,可以观察到类变量的值,但是这样的例子在实践中很少。(这样的例子也可以构造为变量初始化(12.5)。)Java 编程语言的全部功能都可以在这些初始化器中获得;程序员必须小心谨慎。这种能力给代码生成器带来了额外的负担,但是这种负担在任何情况下都会出现,因为 Java 编程语言是并发的(12.4.2)。

例子 12.4.1-1,超类在子类之前初始化:

class Super {
    static { System.out.print("Super "); }
}
class One {
    static { System.out.print("One "); }
}
class Two extends Super {
    static { System.out.print("Two "); }
}
class Test {
    public static void main(String[] args) {
        One o = null;
        Two t = new Two();
        System.out.println((Object)o == (Object)t);
    }
}

此程序输出:

Super Two false

One 永远不会被初始化,因为它没有被主动使用,因此永远不会被链接到。类 Two 仅在其超类 Super 被初始化后才被初始化。

例子 12.4.1-2,只有声明 static 字段的类被初始化:

class Super {
    static int taxi = 1729;
}
class Sub extends Super {
    static { System.out.print("Sub "); }
}
class Test {
    public static void main(String[] args) {
        System.out.println(Sub.taxi);
    }
}

此程序输出:

1729

因为 Sub 类从未初始化;对 Sub.taxi 的引用是对 Super 类中实际声明的字段的引用,不会触发 Sub 类的初始化。

例子 12.4.1-3,接口初始化不初始化超接口:

interface I {
    int i = 1, ii = Test.out("ii", 2);
}
interface J extends I {
    int j = Test.out("j", 3), jj = Test.out("jj", 4);
}
interface K extends J {
    int k = Test.out("k", 5);
}
class Test {
    public static void main(String[] args) {
        System.out.println(J.i);
        System.out.println(K.j);
    }
    static int out(String s, int i) {
        System.out.println(s + "=" + i);
        return i;
    }
}

该程序产生输出:

1
j=3
jj=4
3

J.i 的引用是对作为常量变量的字段引用(4.12.4);因此,他不会导致 I 被初始化(13.4.9)。

K.j 的引用是对接口 J 中实际声明的不是常量变量的字段的引用;这导致接口 J 的字段初始化,但不初始化其超接口 I 的字段,也不初始化接口 K 的字段。

尽管名称 K 用于引用接口 J 的字段 j,但接口 K 并未初始化。

12.4.2 Detailed Initialization Procedure

因为 Java 编程语言是多线程的,所以类或接口的初始化需要小心的同步,因为其他一些线程可能同时视图初始化相同的类或接口。还存在这样的可能性,即类或接口的初始化可以作为该类或接口初始化的一部分被递归地请求;例如,类 A 中的变量初始化器可能会调用不相关的类 B 的方法,这有可能会调用类 A 的方法。

该过程假设 Class 对象已经被验证和准备,并且该对象包含指示四种情况之一的状态:

  • Class 对象已经过验证和准备,但尚未初始化。
  • Class 对象正在被某个特定的线程 T 初始化。
  • Class 对象已经完全初始化,可以使用了。
  • Class 对象处于错误状态,可能是因为初始化尝试失败。

对于每个类或接口 C,都有一个唯一的初始化锁 LC。从 CLC 的映射由 Java 虚拟机实现决定。初始化 C 的过程如下:

  1. C 的初始化锁 LC 进行同步,这包括等待,直到当前线程可以获取 LC
  2. 如果 CClass 对象指示某个其他线程正在对 C 进行初始化,那么释放 LC 并阻塞当前线程,直到通知正在进行的初始化已经完成,此时重复该步骤。
  3. 如果 CClass 对象指示当前线程正在对 C 进行初始化,那么这一定是一个递归的初始化请求。释放 LC 并正常完成。
  4. 如果 CClass 对象指示 C 已经被初始化,那么不需要进一步的动作。释放 LC 并正常完成。
  5. 如果 CClass 对象处于错误状态,那么初始化是不可能的。释放 LC 并抛出一个 NoClassDefFoundError
  6. 否则,记录当前线程正在初始化 CClass 对象,并释放 LC

然后,初始化 Cstatic 字段,它们是常量变量(4.12.4,8.3.2,9.3.1)。

  1. 接下来,如果 C 是一个类而不是一个接口,它的超类还没有初始化,那么设 SC 是它的超类,设 SI1,... ...,SIn是声明了至少一个默认方法的 C 的所有超接口。超接口的顺序由 C 直接实现的每个接口的超接口层次结构上的递归枚举给出(按照 Cimplements 子句从左到右的顺序 )。对于 C 直接实现的每个接口 I ,枚举在返回 I 之前在 I 的超接口上递归(按照 Iextends 子句从左到右的顺序)。

对于列表 [SC,SI1,... ...,SIn],递归地对 S 执行整个过程,如有必要,首先验证并准备 S

如果 S 的初始化因为一个抛出的异常而突然完成,那么获取 LC,将 C 的类对象标记为错误,通知所有等待的线程,释放 LC,然后突然完成,抛出与初始化 S 相同的异常。

  1. 接下来,通过查询 C 的定义类加载器来确定 C 是否启用了断言(14.10)。
  2. 接下来,按照文本顺序执行类的类变量初始值设定项和静态初始值设定项,或者接口的字段初始值设定项,就好像它们是单个块一样。
  3. 如果初始化器的执行正常完成,那么获取 LC, 将 CClass 对象标记为完全初始化,通知所有等待的线程,释放 LC,然后正常完成这个过程。
  4. 否则,初始值设定项一定是通过抛出某个异常 E 而突然完成的,如果 E 的类不是 Error 或它的某个子类,则创建 ExceptionInInitializerError 类的新实例,使用 E 作为参数,并在下面的步骤中使用此对象代替 E。如果由于发生 OutOfMemoryError 而无法创建 ExceptionInInitializerError 的新实例,则在下面的步骤中使用 OutOfMemoryError 对象代替 E
  5. 获取 LC,将 CClass 对象标记为错误,通知所有等待的线程,释放 LC,并突然完成此过程,原因为 E 或其替换,如前一步骤中所确定的。

当实现可以确定类的初始化已经完成时,它可以通过取消步骤 1 中的锁获取(以及步骤 4/5 中的释放)来优化该过程,前提是,就存储器模型而言,如果锁被获取,则所有的 happens-before 排序在执行优化时仍然存在。

代码生成器需要保留类或接口的可能初始化点,插入刚才描述的初始化过程的调用。如果这个初始化过程正常完成,并且 * Class * 对象被完全初始化并准备好使用,那么初始化过程的调用不再是必要的,并且它可以从代码中消除——例如,通过修补它或以其他方式重新生成代码。

在某些情况下,如果可以确定一组相关类型的初始化顺序,编译时分析可能能够从生成代码中消除许多类型已初始化的检查。然而,这种分析必须充分考虑到并发性和初始化代码不受限制的事实。

12.5 Creation of New Class Instances

当对一个类实例创建表达式求值(15.9)导致一个类被实例化时,一个新的类实例被显示地创建。

在以下情况下可以隐式创建一个新的类实例:

  • 加载一个包含 String 字面量(3.10.5)的类或接口可能会创建一个新的 String 对象来表示该字面量。(如果同一个 String 之前已经被保留,这可能不会发生(3.10.5)。)
  • 导致装箱转换的操作的执行(5.1.7)。装箱转换可以创建于原语类型之一相关联的包装类的新对象。
  • 执行不属于常量表达式(15.28)的字符串连接运算符 + (15.18.1)时,总是会创建一个新的字符串来表示结果。字符串串联运算也可以为原始类型的值创建临时包装对象。
  • 评估方法引入表达式(15.13.3)或 lambda 表达式(15.27.4)可能需要创建实现函数式接口类型的类的新实例。

作为类实例创建过程的一部分,这些情况中的每一种都标识了一个特定的构造函数(8.8),改构造函数将使用指定的参数(可能没有)来调用。

每当一个新的类实例被创建时,内存空间被分配给它,其中包括在类类型中声明所有实例变量,以及在类类型的每个超类中声明的所有实例变量,包括所有可能隐藏的实例变量(8.3)。

如果没有足够的可用空间分配内存,那么类实例的创建就会突然结束,并发出 OutOfMemoryError。否则,新对象中的所有实例变量,包括在超类中声明的实例变量,都被初始位它们的默认值(4.12.5)。

就在返回新创建对象的引用作为结果之前,处理指定的构造函数,使用以下过程初始化新对象:

  1. 将构造函数的参数赋给为这个构造函数调用新创建的参数变量。
  2. 如果这个构造函数是从同一个类中的另一个构造函数的显式构造函数调用(8.8.7.1)开始的(使用 this),那么使用这五个步骤计算参数并递归地处理构造函数调用。如果构造函数调用突然完成,那么这个过程也会因为同样的原因而突然完成;否则,继续执行步骤 5。
  3. 此构造函数不以同一个类中的另一个构造函数的显式构造函数调用开始(使用 this)。如果这个构造函数是针对 Object 之外的类,那么这个构造函数将以一个超类构造函数的显式或隐式调用开始(使用 super)。使用相同的五个步骤评估参数并递归处理超类构造函数调用。如果构造函数调用突然完成,那么这个过程也会因同样的原因而突然完成。否则,继续执行步骤 4。
  4. 执行该类的实例初始值设定项和实例变量初始值设定项,将实例变量初始值设定项的值分配给相应的实例变量,按照它们在该类的源代码中以文本形式出现的从左到右的顺序。如果这些初始化器中的任何一个执行导致了异常,俺么就不再处理进一步的初始化器,并且这个过程以同样的异常突然结束。否则,继续执行步骤 5。
  5. 执行该构造函数体的其余部分。如果该执行突然完成,那么该过程也由于同样的原因而突然完成。否则,此过程正常完成。

与 C++ 不同,Java 编程语言在创建新的类实例期间没有为方法分派指定更改规则。如果调用的方法在被初始化的对象的子类中被重写,那么这些重写的方法将被使用,甚至在新对象被完全初始化之前。

例子 12.5-1,实例创建评估:

class Point {
    int x, y;
    Point() { x = 1; y = 1; }
}
class ColoredPoint extends Point {
    int color = 0xFF00FF;
}
class Test {
    public static void main(String[] args) {
        ColoredPoint cp = new ColoredPoint();
        System.out.println(cp.color);
    }
}

这里,创建了一个新的 ColoredPoint 实例。首先,为新的 ColoredPoint 分配空间,以保存字段 xycolor。然后将所有这些字段初始化为它们的默认值(在本例中,每个字段为 0)。接下来,首先调用没有参数的 ColoredPoint 构造函数。由于 ColoredPoint 没有声明构造函数,因此隐式声明了一下形势的默认构造函数:

ColoredPoint() { super(); }

然后,这个构造函数调用不带参数的 Point 构造函数。Point 构造函数并不以调用构造函数开始,因此 Java 编译器提供了对其超类构造函数的隐式调用,不带参数,就像已经编写的那样:

Point() { super(); x = 1; y = 1; }

因此,将调用不带参数的 Object 构造函数。

Object 没有父类,因此递归到此结束。接下来,调用 Object 的任何实例初始化器和实例变量的初始化器。接下来,执行不带参数的 Object 构造函数体。Object 中没有声明这样的构造函数,所以 Java 编译器提供了一个默认构造函数,在这个特殊情况下是:

Object() { }

此构造函数执行无效并返回。

接下来,执行类 Point 的实例变量的所有初始化器。当它发送时,xy 的声明不提供任何初始化值,因此示例的这一步不需要任何操作。然后执行 Point 构造函数体,将 x 设为 1,将 y 设为 1

接下来,执行 ColoredPoint 类的实例变量和初始化器。这一步将值 0xFF00FF 分配给 color。最后,执行 ColoredPoint 构造函数体的其余部分(调用 super 之后的部分);在主题的其他部分中碰巧没有语句,因此不需要进一步操作,初始化完成。

例子 12.5-2,实例创建期间的动态调度:

class Super {
    Super() { printThree(); }
    void printThree() { System.out.println("three"); }
}
class Test extends Super {
    int three = (int)Math.PI;  // That is, 3
    void printThree() { System.out.println(three); }

    public static void main(String[] args) {
        Test t = new Test();
        t.printThree();
    }
}

该程序产生输出:

0
3

这表明在类 Super 的构造函数中调用 printThree 并没有调用类 SuperprintThree 的定义,而是调用了类 TestprintThree 的覆盖定义。因此,该方法在 Test 的字段初始化器执行之前允许,这就是为什么第一个输出值是 0,Test 的字段 three 初始化的默认值。之后在方法 main 中对 printThree 的调用,调用了 printThree 的相同的定义,但此时已经执行了实例变量 three 的初始化器,因此输出了值 3

12.6 Finalization of Class Instances

Object 类有一个 protected 的方法 finalize;这个方法可以被其他类重写。可为对象调用的 finalize 的特定定义成为该对象的终结器(finalizer)。在垃圾收集器回收对象的存储之前,Java 虚拟机将调用该对象的 finalizer 。

finalizer 提供了释放自动存储管理器无法自动释放的资源的机会。在这种情况下,仅仅回收对象使用的内存并不能保证回收对象所持有的资源。

Java 编程语言没有指定调用 finalizer 的时间,只是说将在重用对象的存储志强调用 finalizer。

Java 编程语言没有指定哪个线程将为任何给定对象调用 finalizer。

*重要的是 要注意,许多 finalizer 线程可能是活动的(在大型共享内存多处理器上有时需要这样),如果一个大型连接的数据结构变成垃圾,那么该数据结构中每个对象的所有 * finalize 方法都可能同时被调用,每个 finalizer 调用在不同的线程中运行。

Java 编程语言没有对 finalize 方法调用进行排序。finalizer 可以按任何顺序调用,甚至可以并发调用。

例如,如果循环链接的未终结对象组变得不可达(或 finalizer 可达),则所有对象可以一起变得可终结。最终,这些对象的 finalizer 可以以任何顺序调用,甚至可以使用多线程并发调用。如果自动存储管理器来发现对象不可达,那么它们的存储可以被回收。

实现一个类是很简单的,当所有对象都变得不可访问时,它将导致一组类似 finalizer 的方法以指定的顺序为一组对象调用。定义这样一个类留给读者作为练习。

保证在调用 finalizer 时,调用该 finalizer 的线程不会持有任何用户可见的同步锁。

如果在终结期间引发了未捕获异常,则该异常将被忽略,该对象的终结将终止。

一个对象的构造函数的完成发生在(17.4.5)它的 finalize 方法的执行之前(在 happens-before 的正式意义上)。

在类对象中声明的 finalize 方法不执行任何操作。Object 类声明 finalize 方法的事实意味着任何类的 finalize 方法都可以调用其超类的 finalize 方法。除非程序员有意使超类中 finalizer 的动作无效,否则应该总是这样做。(与构造函数不同,finalizer 不会自动调用超类的 finalizer;这种调用必须显式编码。)

为了提高效率,实现可以跟踪哪些不覆盖 Object类的 finalize 方法的类对象,或者以一种简单 方式覆盖它。

例如:

protected void finalize() throws Throwable {
    super.finalize();
}

12.6.1 所述,我们鼓励实现将这一的对象视为具有未被覆盖的 finalizer,并更有效的终结它们。

可以显示调用 finalizer,就像任何其他方法一样。

java.lang.ref 包描述了弱引用,它与垃圾收集和终结进行交互。与任何与 Java 编程语言有特殊交互的 API 一样,实现者必须了解 java.lang.ref API 提出的任何要求。本规范不以任何方式讨论弱引用。读者可以参考 API 文档了解详细信息。

12.6.1 Implementing Finalization

每个对象都可以用两个属性来描述:它可以是 可到达的(reachable)终结器可到达的(finalizer-reachable)不可到达的(unreachable),也可以是 未终结的(unfinalized)可终结的(finalizable)终结的(finalized)

**可达(reachable)**对象是可以在任何潜在的持续计算中从任何活动线程访问的任何对象。

**终结器可访问(finalizer-reachable)**的对象可以通过某个引用链从某个可终结的对象访问,但不能从任何活动线程访问。

**不可达(unreachable)**对象无论用哪种方法都不可达。

**未终结(unfinalized)**对象从未自动调用其 finalizer。

**终结(finalized)**对象已经自动调用了它的 finalizer。

可终结(finalizable) 对象从未自动调用其终结器,但 Java 虚拟机最终可能会自动调用其终结器。

直到对象 o 的构造函数调用了 o 上层的 Object 的构造函数,并且该调用成功完成(即没有引发异常),对象 o 才是 可终结的(finaliable)。对一个对象的字段的每一个预终结(pre-finalization)写入必须对该对象的终结(finalization)可见。此外,对该对象的字段的预终结(pre-finalization)读取都会看到在该对象的终结被启动之后发生的写入。

程序的优化转换可以设计成减少可到达对象的数量,使之少于那些天真地认为可到达的对象的数量。例如,Java 编译器或代码生成器可能会选择将一个不再使用的变量或参数设置为 null,从而使此类对象的存储可能更快速地被回收。

另一个例子是对象字段中的值存储在寄存器中。然后程序可以访问寄存器而不是对象,并且不在访问该对象。这意味着该对象是垃圾。注意,只有当引用在栈上,而不是存储在堆中时,才允许这种优化。

例如,考虑 Finalizer Guardian 模式:

class Foo {
    private final Object finalizerGuardian = new Object() {
        protected void finalize() throws Throwable {
            /* finalize outer Foo object */
        }
    }
} 

如果子类重写 finalize 并且没有显示调用 super.finalize,finalizer guardian 会强制调用 super.finalize

如果允许对存储在堆上的引用进行这些优化,那么 Java 编译器可以检测到 finalizerGuardian 字段从未被读取,将其清空,理解回收对象,并提前调用 finalizer。这与初衷背道而驰:当 Foo 实例变得不可访问时,程序员可能想调用 Foo 的 finalizer。因此,这种转换是不合法的:只要外部类对象是可达的,内部类对象就应该是可达的。

这种类型的转换可能会导致 finalizer 方法的调用比预期的要早。为了允许用户防止这种情况,我们强调了同步可以保持对象存活的概念。如果一个对象的 finalizer 可以导致该对象上的同步,那么该对象必须是活动的,并且在它被锁定时被认为是可访问的。

请注意,这并不妨碍同步消除:只有当 finalizer 可能对一个对象进行同步时,同步才会使该对象保持活动状态。由于终结器出现在另一个线程中,因此许多情况下,无论如何都无法移除同步。

12.6.2 Interaction with the Memory Model

内存模型(17.4)必须能够决定何时提交发生在 finalizer 中的操作。本节描述 finalization 与内存模型的交互。

每个执行都与许多可达性决策点,标记为 di。每个动作要么发生在 di之前,要么发生在 di 之后。除了明确提到的以外,本节中描述的先来后到排序与内存模型中的所有其他排序无关。

如果 r 是看到写 w 的读,并且 rdi 之前,那么 w 必须在 di 之前。

如果 xy 是对同一变量或监视器的同步操作,使得 so(x, y) (17.4.4)和 ydi 之前,那么 x 必须在 di 之前。

在每个可达性决策点,一些对象集被标记为不可达,而这些对象的一些子集被标记为可终结。这些可达性决策点也是根据 java.lang.ref 包的 API 文件中提供的规则检查、加入队列和清除引用的点。

唯一被认为在 di 点绝对可达的对象是那些可以通过应用这些规则证明可达的对象:

  • 如果存在对类 Cstatic 字段 v 的写入 w1,使得 w1 写入的值是对 B 的引用,类 C 有可到达的类加载器加载,并且不存在对 v 的写入 w2,使得 hb(w2, w1) 不为 true,并且 w1w2 都在 di 之前,则对象 Bdi 处肯定是可达的。
  • 如果存在对 A 的元素 v 的写 w1,使得由 w1 写的值是对 B 的引用,并且不存在对 v 的写 w2 ,使得 hb(w2, w1) 不为 true,并且 w1w2 都在 di 之前,则对象 Bdi 处肯定是从 A 可达的。
  • 如果一个对象 C 从一个对象 B 肯定是可达的,并且对象 B 从一个对象 A 肯定是可达的,那么 CA 肯定是可达的。

如果对象 Xdi 被标记位不可达,则:

  • static 字段到 diX 一定不可达;以及
  • 线程 t 中所有在 di 之后对 X 的所有活动使用必须发生在 X 的 finalizer 调用中,或者线程 tdi 之后执行对 X 的引用的读取的结果;以及
  • 所有在 di 之后的读操作,如果看到对 X 的引用,就必须看到在 di 处不可达的对象元素的写操作,或者在 di 之后看到对象的写操作。

动作 a 是对 X 的主动使用,当且仅当以下至少有一个为 true:

  • 读取或写入 X 的元素
  • a 锁定或解锁 X,并且在调用 X 的 finalizer 之后,会在 X 上发生一个锁定操作
  • a 写入一个对 X 的引用
  • a 是一个对象 Y 的主动使用,XY 肯定是可达的

如果对象 Xdi 被标记为可终结,则:

  • X 必须在 di 处被标记为不可达;以及
  • di 必须是 X 被标记为可终结的唯一位置;以及
  • 在 finalizer 调用之后发生的动作必须在 di 之后。

12.7 Unloading of Classes and Interfaces

Java 编程语言的实现可以卸载类。

当前仅当类或接口的定义类加载可以被垃圾回收期回收时,类或接口才可以被卸载,如 12.6 中所讨论的。

Bootstrap loader 加载的类和接口不能被卸载。

类卸载是一种优化,有助于减少内存使用。显然,程序的语义不应该依赖于系统是否以及如何选择实现优化,比如类卸载。否则会损害程序的可移植性。因此,一个类或接口是否被卸载对程序来说应该是透明的。

然而,如果一个类或接口 C 在它的定义加载器潜在地可达时被卸载,那么 C 可能被重新装载。谁也不能保证这不会发生。即使该类没有被任何其他当前加载的类引用,他也可能被尚未加载的某个类或接口 D 引用。当 D C 的定义加载器加载时,它的执行可能会导致 C 的重新加载。

例如,如果类有 static 变量(其状态会丢失),静态初始值设定项(可能有副作用)或 native 方法(可能保留静态状态),则重新加载可能不透明。此外,类对象的哈希值依赖它的身份。因此,一般来说,以完全透明的方式重新加载一个类或接口是不可能的。

因为我们永远不能保证卸载一个类或接口(其加载器是潜在可达的)不会导致重新加载,重新加载从来都不是透明的,但是卸载必须是透明的,所以当一个类或接口加载器是潜在可达的时候,我们不能卸载它。类似的推理可以用来推断由 Bootstrap loader 装载的类和加快永远不能被卸载。

人们还必须讨论为什么卸载一个类 C 是安全的,如果它的定义类加载器可以被回收的话。如果定义类加载器可以被回收,那么永远不会有对它的任何活动引用(这包括不活动的引用,但可能被 finalizer 复活)。反过来,只有当加载器定义的任何类(包括) C 都不能有任何活动引用时,这种情况才会发送,无论是从它们的实例还是从代码。

类卸载是一种优化,它仅在对加载大量类并在一段时间后停止使用这些类的应用程序有意义。这种应用程序的一个主要例子是 web 浏览器,但还有其他应用程序。这种应用程序的一个特点是,它们通过显式使用类加载器来管理类。因此,上述政策对他们来说非常有效。

严格来说,本规范并没有讨论类卸载的问题,因为类卸载仅仅是一种优化。然而,这个问题非常精妙,因此在此作为澄清提及。

12.8 Program Exit

当发生以下两种情况之一时,程序终止其所有活动并退出:

  • 所有不是守护线程的线程都会终止。
  • 某线程调用类 Runtime 或类 Systemexit 方法,安全管理器不禁止 exit 操作。