本章详述了在程序执行期间发生的活动。围绕 Java 虚拟机和形成程序的类、接口和对象的声明周期来组织它。
通过加载指定的类,然后调用此指定的类中的方法 main ,来启动 Java 虚拟机。12.1 节概述了执行 main 时涉及的加载、链接和初始化步骤,作为本章中概念的介绍。进一步的章节详述了加载、链接和初始化的详情。
本章继续讲述新的类实例(12.5)创建的过程;以及类实例(12.6)的初始化。它通过描述类的卸载和程序退出时所遵循的过程作为结束。
Java 虚拟机通过调用某些指定的类的方法 main ,向此方法传递一个字符串数组参数来启动执行。在本规范的示例中,第一个类通常叫做 Test。
Java 虚拟机启动的准确语义在 Java Virtual Machine Specification(Java SE 8 Edition)的 Chapter 5 给出。在这里,我们从 Java 编程语言的角度介绍了该过程的概述。
向 Java 虚拟机指定初始类的方法超出了本规范的范围,但通常,在使用命令行的宿主环境中,类的完全限定的名称指定为命令行参数,并且将后续的用作字符串的命令行参数作为方法 main 的参数提供。
现在我们将 Java 虚拟机可以采用的执行 Test 步骤作为在后续章节中进一步描述的加载、链接和初始化过程的示例。
初次尝试执行类 Test 的方法 main 发现,没有加载类 Test - 即,Java 当前不包含此类的二进制表示。然后 Java 虚拟机使用类加载器,试图找到这样一个二进制表示。如果此过程失败,则抛出一个错误。此加载过程在 12.2 中进一步描述。
在加载 Test 之后,必须在可以调用 main 之前初始化它。并且像所有(类或接口)类一样,必须在初始化之前链接 Test。链接涉及验证、准备和(可选地)解析。链接在 12.3 中进一步描述。
验证检查,被加载的 Test 的表示是格式良好的,具有正确的符号表。验证也检查,实现 Test 的代码遵循 Java 编程语言和 Java 虚拟机的语义要求。如果验证期间检测到问题,则抛出错误。验证在 12.3.1 中进一步描述。
准备涉及 static 存储和由 Java 虚拟机实现在内部使用的,如方法表,任何数据结构的分配。准备在 12.3.2 中进一步描述。
解析是检查从 Test 到其它类和接口的符号引用的过程,通过加载提及的其它类和接口并检查,引用是正确的。
在初始链接时,解析步骤是可选的。实现可以解析来自非常早链接的类或接口的符号引用,甚至可以递归地解析来自进一步引用到的类或接口的符号引用。(此解析可能从这些进一步加载和链接步骤中产生错误。)这一实现选择是非常极端的实现方式,并且类似于在 C 语言的简单实现中多年来所做的那种“静态”链接。(在这些实现中,被编译的程序通常表示为包含程序的完全链接版本的 "a.out" 文件,包括完整地解析的到由程序使用的库例程的链接。这些库例程的副本包含在 "a.out" 文件中。)
反之,实现可以选择仅当用到符号引用时才解析它们;对所有符号引用一致地使用此策略表示“最懒”的解析形式。在这种情况下,如果 Test 有另一个类的多个符号引用,则这些引用可能一次被解析,因为它们都被使用,亦或者不是,如果程序执行期间从未使用过这些引用。
执行解析时的唯一要求是,必须抛出解析期间检测到的任何错误,在程序中由直接或间接地要求链接到类或接口的程序执行的涉及到此错误的某些操作的点处。使用上面描述的“静态”示例实现选项,加载和链接错误在执行程序之前发生,如果它们涉及在类 Test 中提及的类或接口,或任何进一步的,递归地引用的,类和接口。在实现“最懒”解析的系统中,仅当主动使用不正确的符号引用时才抛出这些错误。
解析过程在 12.3.3 中进一步描述。
在我们持续的示例中,Java 虚拟机仍然尝试执行类 Test 的方法 main。仅当初始化(12.4.1)类之后才允许。
初始化由任何类变量初始化器和类 Test 的静态初始化器的执行组成,以文本中出现的顺序。但在可以初始化 Test 之前,必须先初始化它的父类,以及它的直接父类的直接父类,等等,递归地。在最简单的示例中,Test 将 Object 作为它的隐式的直接父类;如果类 Object 还未初始化,则必须在初始化 Test 之前初始化它。类 Object 没有父类,因此递归在这终止。
如果类 Test 将另一个类 Super 作为它的父类,则在 Test 之前必须先初始化 Super。这要求加载、验证和准备 Super,如果这些还未完成,依赖于实现,并且还涉及来自 Super 的符号引用,等等,递归地。
初始化可能因此导致加载、链接和初始化错误,包括涉及其它类型的此类错误。
初始化过程在 12.4 中进一步描述。
最后,类 Test(在此期间可能发生了其它间接的加载、链接和初始化)的初始化完成之后,调用 Test 的方法 main。
方法 main 必须声明为 public、staic 和 void。它必须指定一个声明类型是 String 数组的形式参数(8.4.1)。因此,以下两个声明都是可接受的:
加载表示查找具有特定名称的类或接口类型的二进制形式的过程,可能通过在百忙中计算它,但更通常的是通过检索以前由 Java 编译器从源代码计算出的二进制表示,并从该二进制形式中构造一个 Class 对象,以表示该类或接口。
加载的精确语义在 Chapter 5 of The Java Virtual Machine Specification, Java SE 8 Edition 中给出。我们在这从 Java 编程语言的角度给出此过程的概述。
类或接口的二进制格式正常地是在以上引用的 The Java Virtual Machine Specification, Java SE 8 Edition 中描述的 class 文件格式,但也可能是其它格式,只要它们满足 13.1 中指定的需求。类 ClassLoader 的方法 defineClass 可用于从 class 文件格式中的二进制表示构造 Class 对象。
行为表现良好的类加载器维护了这些属性:
* 给定相同的名称,优秀的类加载器应该总是返回相同的类对象。
* 如果一个类加载器 L1 委托另一个加载器 L2 加载类 C,则对于任何作为 C 的直接父类或直接父接口出现的类型,或作为 C 中字段的类型,或作为 C 中方法或构造器的形式参数的类型,或 C 中方法的返回类型,L1 和 L2 应该返回相同的 Class 对象。
恶意的类加载器可能会违反这些属性。但是,它不能破坏类型系统的安全,因为 Java 虚拟机会对此进行防范。
有关这些问题的进一步讨论,请参见 The Java Virtual Machine Specification, Java SE 8 Edition 和论文 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 编程语言编写的代码所颠覆,即使是诸如 ClassLoader 和 SecurityManager 之类的其它敏感的系统类的实现。
加载过程由类 ClassLoader 及其子类来实现。
ClassLoader 的不同子类可能实现不同的加载策略。通常,类加载器可以缓存类和接口的二进制表示,根据预期的使用情况来预取它们,或者一起加载一组相关的类。这些活动可能对正在运行的应用程序不完全透明,例如,如果未找到类的新编译的版本,因为旧版本是由类加载器缓存的。但是,仅在程序中出现加载错误的点处反映加载错误,而无需预取或组加载,是类加载器的责任。
如果类加载期间发生错误,则在(直接或间接)使用该类型的程序中的点处抛出类 LinkageError 的下列子类之一的实例:
* ClassCircularityError:类或接口不能被加载,因为它是它自己的父类或父接口(8.1.4,9.1.3,13.4.4)。
* ClassFormatError:意图指定被请求的被编译的类或接口的二进制数据是格式错误的。
* NoClassDefFoundError:通过相关的类加载器找不到被请求的类或接口的定义。
因为加载涉及新数据结构的分配,所以它可能因为 OutOfMemoryError 而失败。
链接是取类或接口的二进制形式并将其合并到 Java 虚拟机的运行时状态的过程,以便可以执行它。总是在链接类或接口类型之前先加载它。
链接中涉及三个不同的活动:验证、准备和符号引用的解析。
链接的精确语义在中 Chapter 5 of The Java Virtual Machine Specification, Java SE 8 Edition 给出。我们从 Java 编程语言的角度介绍了该过程的概述。
此规范允许在链接活动(由于递归、加载)发生时的灵活性,只要遵守了 Java 编程语言的语义,完整地验证了类或接口并在初始化之前准备它,并在链接期间抛出检测到的错误,在由要求链接类或接口的涉及此错误的程序采取的操作的程序中的点处。
例如,实现可以选择单独解析类或接口中的每个符号引用,仅当使用它时(延迟解析),或在验证类时(静态解析)一次解析它们所有。在某些实现中,这意味着解析过程可以在初始化类或接口之后继续。
因为链接涉及新数据结构的分配,所以它可能因为 OutOfMemoryError 而失败。
验证确保类或接口的二进制表示结构上是正确的。例如,它检查,每个指令具有合法的操作码;每个分支指令跳转到某些其它指令的起始处,而不是指令的中间;每个方法具有结构上正确的签名;每个指令遵循 Java 虚拟机的类型规则。
如果验证期间发生错误,则在程序中导致类被验证的点处将抛出类 LinkageError 的下列子类的实例:
* VerifyError:类或接口的二进制定义未能传递一组所需的检查,以验证它是否遵从 Java 虚拟机的语义,并且它不能违反 Java 虚拟机的完整性。(请参见 13.4.2、13.4.4、13.4.9,有关一些示例,请参见 13.4.17。)
准备涉及为类或接口创建 static 字段(类变量和常量),将此类字段初始化为默认值(4.12.5)。这并未要求任何源代码的执行; static 字段的显式初始化器作为初始化的一部分执行(12.4),而不是准备的。
Java 虚拟机的实现可能在准备时预先计算额外的数据结构,以使类或接口上的后续操作更有效率。一个特别有用的数据结构是“方法表”或其它允许在类的实例上调用任何方法而不需要调用时父类搜索的任何方法的数据结构。
类或接口的二进制表示使用其它类和接口(13.1)的二进制名称(13.1),象征性地引用其它类和接口,和它们的字段、方法和构造器。对于字段和方法, 这些符号引用包含字段或方法所属的类或接口的名称,以及字段或方法本身的名称以及相应的类型信息。
在可以使用符号引用之前,它必须经历解析,其中检查符号引用是正确的,并且通常用可以更有效地处理的直接引用替换,如果重复地使用引用。
如果解析期间发生错误,则将抛出错误。通常,这将是类 IncompatibleClassChangeError 的下列子类之一的实例,但它也可能是 IncompatibleClassChangeError 的某些其它子类的实例,甚至类 IncompatibleClassChangeError 本身的实例。可以在直接或间接使用该类型的符号引用的程序中的任何点处抛出此错误:
* IllegalAccessError:遇到一个符号引用,其向不具有访问权限的包含此引用的代码,指定一个使用或字段的赋值,或方法调用,或类的实例的创建,由于用 private、protected 或 包访问(不是 public)声明字段或方法,或由于类未声明为 public。
* InstantiationError:遇到一个在类实例创建表达式中使用的符号引用,但无法创建实例,因为此引用结果是引用接口或 abstract 类。
例如,如果最初不是 abstract 的类在另一个引用这个类的类被编译(13.4.1)之后被更改为 abstract,则可能发生此问题。
* NoSuchFieldError:遇到一个引用具体类或接口的具体字段的符号引用,但该类或接口不包含该名称的字段。
例如,如果在引用字段的另一个类被编译(13.4.8)后从类中删除了字段声明,则会发生这种情况。
* NuSuchMethodError:遇到一个引用具体类或接口的具体方法的符号引用,但类或接口不包含该名称的方法。
例如,如果在引用方法的另一个类被编译(13.4.12)后从类中删除了方法声明,则可能发生这种情况。
此外,如果类声明了一个找不到实现的 native 方法,则抛出 UnsatisfiedLinkError,LinkageError 的子类。
如果使用该方法,或更早,则会发生错误,取决于 Java 虚拟机(12.3)实现使用的策略的种类。
类的初始化由执行它的静态初始化器和类中声明的 static 字段(类变量)的初始化器组成。
接口的初始化由执行接口中声明的字段(常量)的初始化器组成。
类或接口类型 T 在以下任一出现时直接初始化:
* T 是类,并且创建 T 的实例。
* 调用由 T 声明的 static 方法。
* 给由 T 声明的 static 字段赋值。
* 使用由 T 声明的 static 字段,并且该字段不是常变量(4.12.4)。
* T 是顶层类,并且执行词法上嵌套在 T(8.1.3)中的 assert 语句(14.10)。
当初始化类时,初始化它的父类(如果它们以前没有初始化过),以及声明任何 default 方法的任何父接口(8.1.5)(如果它们以前没有初始化过)。接口本身的初始化不会导致任何它的父接口的初始化。
static 字段(8.3.1.1)的引用导致仅实际声明它的类或接口的初始化,即使可能通过子类、子接口或实现接口的类的名称来引用它。
类 Class 和 包 java.lang.reflect 中某些反射方法的调用也会导致类或接口的初始化。
在任何其它情况下,类或接口都不会初始化。
注意,编译器可能在接口中声明 synthetic default 方法,即,既不是显式地也不是隐式地声明(13.1)的 default 方法。此类方法将会触发接口的初始化,尽管源代码没有给出任何指示,该接口应该被初始化。
其目的是,类或接口类型具有一组将其放在一致状态中的初始化器,并且此状态是被其它类观察到的第一个状态。静态初始化器和类变量初始化器以原文中的顺序执行,并且不能引用在其声明在原文中出现在使用之后的类中声明的类变量,即使这些类变量在作用域中(8.3.3)。这一限制旨在在编译时检测大多数循环或其它格式错误的初始化。
初始化代码不受限制这一事实允许构造这样的示例,其中可以观察到类变量的值当它仍然具有它的初始默认值时,在计算它的初始化表达式之前,但这样的示例实际上是罕见的。(也可以为实例变量初始化(12.5)构造此类示例。)Java 编程语言的全部力量在这些初始化器中可用;程序员必须谨慎。这一力量在代码生成器上放置了额外的负担,但这一负担在任何情况下都会产生,因为 Java 编程语言是并发的(12.4.2)。
由于 Java 编程语言是多线程的,因此,类或接口的初始化要求小心的同步,因为某些其它线程可能同时正在尝试初始化相同的类或接口。类或接口的初始化也有可能作为该类或接口的初始化的一部分被递归地请求;例如,类 A 中的变量初始化器可能调用不相关的类 B 的方法,其反过来又调用类 A 的方法。Java 虚拟机的实现有责任使用以下过程来处理同步和递归初始化。
过程假定 Class 对象已被验证和准备,并且 Class 对象包含指示四种情形之一的状态:
* 此 Class 对象已被验证和准备,但未初始化。
* 此 Class 对象正在被某些特定的线程 T 初始化。
* 此 Class 对象已完全初始化,并准备使用。
* 此 Class 对象处于错误的状态,可能由于已尝试初始化,但失败了。
对于每个类或接口 C,都有一个唯一的初始化锁 LC。从 C 到 LC 的映射由 Java 虚拟机实现决定。然后,初始化 C 的过程如下:
-
在初始化锁 LC 上同步 C。这涉及等待直到当前线程可以获取 LC。
-
如果 C 的 Class 对象指示,C 的初始化正在由某些其它线程处理,则释放 LC 并阻塞当前线程,直到通知处理中的初始化已经完成,此时将重复此步骤。
-
如果 C 的 Class 对象指示,C 的初始化正在由当前线程处理,则这必须是初始化的递归请求。释放 LC 并正常地完成。
-
如果 C 的 Class 对象指示,C 已完成初始化,则不需要进一步的操作。释放 LC 并正常地完成。
-
如果 C 的 Class 对象正处于错误的状态,则不可能初始化。释放 LC 并抛出 NoClassDefFoundError。
-
否则,记录 C 的 Class 对象的初始化正在由当前线程处理这一事实,并释放 LC。
然后,初始化 C 的为常变量(4.12.4,8.3.2,9.3.1)的 static 字段。
-
下一步,如果 C 是类而不是接口,并且其父类还未初始化,则让 SC 是其父类,让 SI1, ..., SIn 是 C 的声明至少一个 default 方法的父接口。父接口的顺序由在直接由 C 实现的每个接口的父接口层次结构上的递归枚举给定(以 C 的 implements 子句的从左到右的顺序)。对于由 C 直接实现的每个接口 I,返回 I 之前在 I 的父接口上(以 I 的 extends 子句的从左到右的顺序)反复枚举。
对于列表 [ SC, SI1, ..., SIn ] 中的每个 S,对 S 递归地执行这一完整过程。如有必要,先验证、准备 S。
如果 S 的初始化由于抛出异常而突然完成,则获取 LC,把 C 的 Class 对象标记为错误的,通知所有等待的线程,释放 LC,并突然完成,抛出初始化 S 产生的相同异常。
-
下一步,确定是否启用(14.10)了 C 的断言,通过查询它的定义类加载器。
-
下一步,执行类的类变量初始化器和静态初始化器,或接口的字段初始化器,以原文中的顺序,就好像它们是单个块一样。
-
如果初始化器的执行正常完成,则获取 LC,将 C 的 Class 对象标记为已完全初始化,通过所有等待的线程,释放 LC,并正常地完成此过程。
-
否则,初始化器必须通过抛出某些异常 E 来突然完成。如果 E 的类不是 Error 或其子类之一,则创建一个类 ExceptionInInitializerError 的新实例,具有 E 作为参数,并在后面的步骤中用这个对象代替 E。如果由于发生 OutOfMemoryError 而无法创建 ExceptionInInitializerError 的新实例,则反之在后面的步骤中用 OutOfMemoryError 对象代替 E。
-
获取 LC,将 C 的 Class 对象标记为错误的,通知所有等待的线程,释放 LC,并带有原因 E 或在前一步中确定的它的替代突然完成此过程。
实现可能通过省略步骤 1 中的锁获取(和步骤 4/5 中的释放)来优化此过程,当它可以确定类的初始化已完成时,假设,根据内存模型,如果获得了锁,所有存在的 happens-before 顺序仍然存在,当优化执行时。
代码生成器需要保留类或接口的可能初始化点,插入刚才描述的初始化过程的调用。如果此初始化过程正常完成,并且 Class 对象已完全初始化并准备好使用,则不再需要调用初始化过程,并可以从代码中消除它 - 例如,通过修补它或重新生成代码。
在某些情况下,编译时分析可以消除从声明的代码中已初始化的类型的许多检查,如果可以确定一组相关类型的初始化顺序。但是,这种分析必须充分考虑并发性和初始化代码不受限制这一事实。
新的类实例被显式地创建,当类实例创建表达式(15.9)的计算导致类被实例化时。
在以下情况下,可以隐式地创建新的类实例:
* 包含 String 字面量(3.10.5)的类或接口的加载可以创建一个新的 String 对象,以表示该字面量。(如果相同的 String 先前已被拘束(3.10.5),则不会发生这个。)
* 导致装箱转换(5.1.7)的操作的执行。装箱转换可以创建与基元类型之一相关联的包装器类的新对象。
* 不是常量表达式(15.28)一部分的字符串串联操作符 +(15.18.1)的执行总是创建新的 String 对象,以表示结果。字符串串联操作符也可能创建基元类型值的临时的包装器对象。
* 方法引用表达式(15.13.3)或 lambda 表达式(15.27.4)的计算可以要求,创建实现函数式接口类型的类的新实例。
这些情况中的每一个都标识用特定参数(可能没有)调用的构造器,作为类实例创建表达式的一部分。
每当创建新的类实例时,就会为它分配内存空间,并为类类型中声明的所有实例变量和类类型的每个父类中声明的实例变量提供空间,包括所有可能被隐藏的实例变量。
如果没有足够可用的空间为对象分配内存,则类实例的创建使用 OutOfMemoryError 突然完成。否则,新对象中的所有实例变量,包括父类中声明的那些,被初始化为它们的默认值(4.12.5)。
在将新创建的对象的引用作为结果返回之前,指示的构造器被处理,以使用以下过程初始化新对象:
-
将构造器的参数赋值给新创建的此构造器调用的参数变量。
-
如果此构造器以同一类(使用 this)中的另一个构造器的显式构造器调用(8.8.7.1)开头,则计算参数并递归地使用这些相同的五个步骤来处理该构造器调用。如果该构造器调用突然完成,则此过程因相同的原因而突然完成;否则,继续步骤 5。
-
此构造器不以同一类(使用 this)中的另一个构造器的显式构造器调用开头。如果此构造器是除 Object 以外的类的,则此构造器将以父类构造器(使用 super)的显式或隐式地调用开头。计算参数并递归地使用这些相同的五个步骤来处理该父类构造器。如果该构造器调用突然完成,则此过程因相同的原因而突然完成。否则,继续步骤 4。
-
执行此类的实例初始化器和实例变量初始化器,将实例变量初始化器的值赋值给相应的实例变量,以类的源代码中它们在原文中以从左到右出现的顺序。如果这些初始化器的任何执行产生异常,则不进一步处理初始化器,并且此过程以相同的异常突然完成。否则,继续步骤 5。
-
执行此构造器的 body 的余下部分。如果该执行突然完成,则此过程因相同的原因突然完成。否则,此过程正常完成。
与 C++ 不同,Java 编程语言在创建新的类实例期间没有方法调度指定更改的规则。如果在被初始化的对象中调用子类中重写的方法,则使用这些重写方法,即使在新对象完全初始化之前。
类 Object 具有一个叫做 finalize 的 protected 方法;此方法可以被其它类重写。可以为对象调用的 finalize 的详细定义被称为该对象的终结器。在对象的存储被垃圾收集器回收之前,Java 虚拟机先调用该对象的终结器。
终结器提供了释放无法被自动存储管理器释放的资源的机会。在这种情况下,简单地回收由对象使用的内存无法保证它持有的资源会被回收。
Java 编程语言未指定多久以后调用终结器,除了说它将在重用对象的存储之前发生。
Java 编程语言未指定哪个线程将调用任何给定对象的终结器。
注意,许多终结器可能是活动的(这有时在大的共享存储器的多处理器上是必需的),这是非常重要的,并且如果大的有联系的数据结构变成垃圾,将同时调用该数据结构中的每个对象的 finalize 方法,每个终结器调用运行在一个不同的线程中。
当所有对象变成不可达时,实现一个将导致以为一组对象指定的顺序调用一组类似终结器的方法的类,是非常简单的。定义这样的类将留给读者练习。
可以保证调用终结器的线程在调用终结器时不会持有任何用户可见的同步锁。
如果终结期间抛出捕获的异常,则忽略此异常且该对象的终结终止。
对象的构造器的完成在它的 finalize 方法(在标准的 happens-before 场景中)的执行之前发生(17.4.5)。
在类 Object 中声明的 finalize 方法不采取任何操作。类 Object 声明 finalize 方法的这一事实意味着任何类的 finalize 方法总是可以调用其父类的 finalize 方法。应该总是这样做,除非程序员想要注销父类中的终结器的操作。(与构造器不同,终结器不会自动地调用父类的终结器;此类调用必须显式地编码。)
为了提高效率,实现可以跟踪不重写类 Object 的 finalize 方法的类,或以不重要的方式重写它。
我们鼓励实现将此类对象视为具有未重写的终结器,并更有效地终结它们,如 12.6.1 中所述。
可以显式地调用构造器,就像任何其它方法一样。
包 java.lang.ref 描述了弱引用,其与垃圾收集器和终结进行交互。如同与 Java 编程语言具有特殊交互作用的任何 API 一样,实现者必须认识到由 java.lang.ref API 强加的任何要求。此规范不以任何方式讨论弱引用。读者可以参考 API 文档以了解详细信息。
每个对象都可以有两个属性:它可以是可达的、终结器可达的或不可达的,并且它也可以是未终结的、可终结的或已终结的。
可达的对象是在任何存活的线程的任何可能的持续计算中可访问的任何对象。
终结器可达的对象是通过某些引用链从某些可终结的对象可达的,而不是从任何存活的线程。
不可达的对象通过两者中任何一个方法都无法到达。
未终结的对象从未自动地调用过其终结器。
已终结的对象已经自动地调用过其终结器。
可终结的对象从未自动地调用过其终结器,但 Java 虚拟机最终会自动地调用它的终结器。
对象 o 不是可终结的,直到它的构造器在 o 上调用过 Object 的构造器,并且该调用成功完成(即,未抛出异常)。字段的每个预终结的写必须对该对象的终结是可见的。此外,字段的预终结的读不可以看到在开始该对象的终结之后发生的写。
程序的优化转换可以设计为,将可达的对象的数量减少到比那些天真地被认为可达的对象的数量少。例如,Java 编译器或代码生成器可以选择将不再使用的变量或参数设置为 null,以导致此类对象的存储可能被很快地回收。
如果对象的字段中的值存储在寄存器中,则会出现此情况的另一个示例。然后程序可以访问寄存器而不是对象,并且不再访问该对象。这意味着该对象是垃圾。注意,当引用位于栈上而不存储在堆中时,才允许此类优化。
存储器模型(17.4)必须可以确定它什么时候可以提交在终结器中发生的操作。本节描述了终结与存储器模型的交互。
每个执行都有一些可达性决策点,记为 di。每个操作要么在 di 之前出现,要么在 di 之后出现。除明确提到的以外,本节中描述的 comes-before 顺与与内存模型中的所有其它顺序无关。
如果 r 是一个看到一个写的读,并且 r 在 di 之前出现,则 w 必须 在 di 之前出现。
如果 x 和 y 是同一个变量或管程上的同步操作,这样 so(x,y)(17.4.4),并且 y 在 di 之前出现,则 x 必须 在 di 之前出现。
在每个可达性决策点处,某些对象集被标记为不可达的,这些对象的某些子集被标记为可终结的。这些可达性决策点也是根据包 java.lang.ref 的 API 文档中提供的规则检查、排队和清除引用的点。
在点 di 处被明确视为可达的对象是那些可以通过这些规则的应用被显示为可达的对象:
* 如果存在到类 C 的 static 字段 v 的写 w1,而由 w1 写的值是到对象 B 的引用,类 C 由可达的类加载器加载,并且不存在到 v 的写 w2,这样 hb(w2,w1) 不为 true,并且 w1 和 w2 两者都在 di 之前出现,则对象 B 在 di 处从此 static 字段是明确可达的。
* 如果有到 A 的元素 v 的写 w1,而由 w1 写的值是到 对象 B 的引用,并且不存在到 v 的写 w2,这样 hb(w2,w1) 不为 true,并且 w1 和 w2 两者都在 di 之前出现,则对象 B 从 A 在 di 处是明确可达的。
* 如果对象 C 从对象 B 是明确可达的,并且对象 B 从对象 A 是明确可达的,则 C 从 A 是明确可达的。
如果对象 X 在 di 处被标记为不可达的,则:
* X 在 di 处从 static 字段不能是明确可达的;并且
* 线程 t 中在 di 之后出现的 X 的所有活动的使用必须在 X 的终结器调用中或作为执行在 di 之后出现的 X 的引用的读的线程 t 的结果出现;并且
* 所有在 di 之后出现的看到 X 的引用的读必须看到在 di 处不可达的对象的元素的写,或看到在 di 之后出现的写。
操作 a 是 X 的活动的使用,当且仅当以下至少一个为 true:
* a 读或写 X 的元素
* a 锁定或解锁 X ,并且有一个在 X 的终结器的调用之后发生的在 X 上的锁定操作
* a 写 X 的引用
* a 是对象 Y 的活动的使用,并且 X 从 Y 是明确可达的
如果对象 X 在 di 处被标记为可终结的,则:
* X 在 di 处必须被标记为不可达的;并且
* di 必须是唯一的 X 被标记为可终结的的位置;并且
* 在终结器调用之后发生的操作必须在 di 之后发生。
Java 编程语言的实现必须卸载类。
类或接口可以被卸载,当且仅当它的定义类加载器可以被 12.6 中所讨论的垃圾收集器回收时。
由引导加载器加载的类和接口不可以被卸载。
类卸载是帮助减少内存使用的优化。显然,程序的语义不应依赖系统是否和如何选择实现优化,例如类卸载。否则,这样做会损害程序的可移植性。因此,类或接口是否已卸载不应对程序透明。
但是,如果类或接口 C 被卸载,而它的定义加载器是潜在可达的,则 C 可以会被重新加载。永远不能保证这不会发生。即使该类没有被任何其它当前加载的类引用,它可能被某些还未加载的类或接口,D,引用。当 D 被 C 的定义加载器加载时,它的执行可能导致 C 的重新加载。
重新加载可能不是透明的,例如,如果类有静态变量(其状态会丢失)、静态初始化器(其可能有副作用)或 native 方法(其可能保持静止状态)。此外,Class 对象的哈希值依赖它的标识。因此,一般不可能以完全透明的方式重新加载类或接口。
因为我们不能保证卸载其加载器是潜在可达的的类或接口将不会导致重新加载,而重新加载永远不是透明的,但卸载必须是透明的,它遵循,不能在其加载器是潜在可达的时卸载类或接口。类似的推理可以用来推断,由引导加载器加载的类和接口永远不能被卸载。
还必须讨论,为什么,如果其定义加载器可以被回收,卸载类 C 是安全的。如果定义加载器可以被回收,则不会有任何存活的对它的引用(这包括不存在的引用,但可以由终结器复活)。反过来,当没有任何存活的到由该加载器定义的任何类,包括 C,的引用,无论是从它们的实例或代码,这才为 true。
类卸载是一种优化,对于加载大量类并且在一段时间后停止使用那些类中的大多数的应用来说,这一点非常重要。此类应用程序的一个典型示例是 web 浏览器,但还有其它的。此类应用程序的一个特征是,它们通过显式地使用类加载器来管理类。因此,上文所述的策略对它们很有效。
严格说来,由本规范讨论的类卸载问题并不重要,因为类卸载只是一种优化。但是,这个问题是非常微妙的,所以以澄清的方式在这里提出。
程序终止所有它的活动并退出,当两件事之一发生时:
* 所有不是守护线程的线程终止。
* 某些线程调用类 Runtime 或类 System 的 exit 方法,并且此 exit 操作没有被安全管理器禁止。