堆碎块是在正常的程序运行过程中产生的。新的对象分配了空间,不再被引用的对象被释放,所以堆内存的空闲位置介于活 动的对象之间。请求分配新对象时可能不得不增大堆空间的大小,虽然可以使用的总空闲空间 是足够的,这是因为,堆中没有连续的空闲空间放得下新的对象。在一个虚拟内存系统中,增 长的堆所需要的额外分页(或交换)空间会影响运行程序的性能。在内存较小的嵌入式系统中, 碎块导致虚拟机产生不必要的“内存不足”错误。
9.2垃圾收集算法
任何垃圾收集算法都必须做两件事情。首先,它必须检测出垃圾对象。其次,它必须回收 垃圾对象所使用的堆空间并还给程序。
垃圾检测通常通过建立一个根对象的集合并且检查从这些根对象开始的可触及性来实现。 如果正在执行的程序可以访问到的根对象和某个对象之间存在引用路径,这个对象就是可触及 的。对于程序来说,根对象总是可以访问的。从这些根对象开始,任何可以被触及的对象都被 认为是“活动”的对象。无法被触及的对象被认为是垃圾,因为它们不再影响程序的未来执行。
Java虚拟机的根对象集合根据实现不同而不同,但是总会包含局部变量中的对象引用和栈帧 的操作数栈(以及类变量中的对象引用)。另外一个根对象的来源是被加载的类的常量池中的对象引用,比如字符串。被加载的类的常量池可能指向保存在堆中的字符串,比如类名字,超类的名字,超接口的名字,字段名,字段特征签名,方法名或者方法特征签名。还有一个来源是 传递到本地方法中的、没有被本地方法“释放”的对象引用。(根据本地方法接口,本地方法可 以通过简单地返回来释放引用,或者显式地调用一个回调函数来释放传递来的引用,或者是这 两者的结合。)再一个潜在的根对象的来源就是,Java虚拟机运行时数据区中从垃圾收集器的堆中分配的部分。举例来说,在某些实现中,方法区中的类数据本身可能被存放在使用垃圾收集 器的堆中,以便使用和释放对象同样的垃圾收集箅法来检测和卸载不再被引用的类。
任何被根对象引用的对象都是可触及的,从而是活动的。另外,任何被活动的对象引用的 对象都是可触及的。程序可以访问任何可触及的对象,所以这些对象必须保留在堆里面。任何 不可触及的对象都可以被收集,因为程序没有办法来访问它们。
在Java虚拟机实现中,有些垃圾收集器可以区别真正的对象引用和看上去像合法对象引用的 基本类塑(比如一个int)之间的差别。(例如一个int整数,如果被解释是一个本地指针,可能指 向堆中的一个对象。)可是某些垃圾收集器仍然选择不区分真正的对象引用和“伪装品”,这种 垃圾收集器被称为保守的(conservative ),因为它们可能不能总是释放每一个不再被引用的对象。 对保守的收集器,有时候垃圾对象也被错误地判断为活动的,因为有一个看上去像是对象引用 的基本类型“引用” 了对象。保守的收集器使垃圾收集速度提高了,因为有一些垃圾被遗忘了。
区分活动对象和垃圾的两个基本方法是引用计数和跟踪。引用计数垃圾收集器通过为堆中 的每一个对象保存一个计数来区分活动对象和垃圾对象。这个计数记录下了对那个对象的引用 次数。跟踪垃圾收集器实际上追踪从根结点开始的引用图。在追踪中遇上的对象以某种方式打
上标记,当追踪结束时,没有被打上标记的对象就被判定是不可触及的,可以被当作垃圾收集。
9.3引用计数收集器
引用计数是垃圾收集的早期策略。在这种方法中,堆中每一个对象都有一个引用计数。当 一个对象被创建了,并且指向该对象的引用被分配给一个变量,这个对象的引用计数被置为1。
当任何其他变量被陚值为对这个对象的引用时,计数加1。当一个对象的引用超过了生存期或者 被设置一个新的值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当 一个对象被垃圾收集的时候,它引用的任何对象计数值减1。在这种方法中,一个对象被垃圾收 集后可能导致后续其他对象的垃圾收集行动。
这种方法的好处是,引用计数收集器町以很快地执行,交织在程序的运行之中。这个特性 对于程序不能被长时间打断的实时环境很有利。坏处就是,引用计数无法检测出循环(即两个 或者更多的对象互相引用)。循环的例子如,父对象有一个对子对象的引用,子对象又反过来引 用父对象。这些对象永远都不可能计数为0,就算它们已经无法被执行程序的根对象可触及。还 有一个坏处就是,每次引用计数的增加或者减少都带来额外开销。
因为引用计数方法固有的缺陷,这种技术现在已经不为人所接受。现实生活中所遇到的Java 虚拟机更有可能在垃圾收集堆中使用追踪算法。
9.4跟踪收集器
跟踪收集器追踪从根结点开始的对象引用图。在追踪过程中遇到的对象以某种方式打上标 记。总的来说,要么在对象本身设置标记,要么用一个独立的位图来设置标记。当追踪结束时,未被标记的对象就知道是无法触及的,从而可以被收集。
基本的追踪算法被称作"标记并淸除”。这个名字指出垃圾收集过程的两个阶段。在标记阶 段,垃圾收集器遍历引用树,标记每一个遇到的对象。在清除阶段,未被标记的对象被释放了, 使用的内存被返回到正在执行的程序。在java虚拟机中,清除步骤必须包括对象的终结。
9.5压缩收集器
Java虚拟机的垃圾收集器可能有对付堆碎块的策略。标记并清除收集器通常使用的两种策略 是压缩和拷贝。这两种方法都是快速地移动对象来减少堆碎块。压缩收集器把活动的对象越过 空闲区滑动到堆的一端,在这个过程中,堆的另一端出现个大的连绫空闲区。所有被移动的 对象的引用也被更新,指向新的位置。
更新被移动的对象的引用有时候通过一个间接对象引用层可以变得更简单。不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表。对象句柄才指向堆中对象的实际位置。当 对象被移动了,只有这个句柄需要被更新为新位置。所有的程序中对这个对象的引用仍然指向 这个具有新值的句柄,而句柄本身没有移动。这种方法简化了消除堆碎块的工作,但是每一次 对象访问都带来了性能损失。
9.6拷贝收集器
拷贝垃圾收集器把所有的活动对象移动到一个新的区域。在拷贝的过程中,它们被紧挨着布置,所以可以消除原本它们在旧区域的空隙。原有的区域被认为都是空闲区。这种方法的好 处是对象可以在从根对象开始的遍历过程中随着发现而被拷贝,不再有标记和清除的区分。对 象被快速拷贝到新区域,同时转向指针仍然留在原来的位置。转向指针可以让垃圾收集器发现 已经被转移的对象的引用。然后垃圾收集器可以把这些引用设置为转向指针的值,所以它们现
在指向对象的新位置。
—般的拷贝收集器算法被称为“停止并拷贝“。 在这个方案中,堆被分为两个区域,任何时 候都只使用其中的一个区域。对象在同一个区域中分配,直到这个区域被耗尽.:此时,程序执 行被中止,堆被遍历,遍历时遇到的活动对象被拷贝到另外一个区域。当停止和拷贝过程结束 时,程序恢复执行。内存将从新的堆区域中分配,直到它也被用尽。那时程序将再次中止,遍 历堆,活动对象又被拷贝回原来的区域。这种方法带来的代价就是,对于指定大小的堆来说需 要两倍大小的内存,因为任何时候都只能使用其中的一半。
在图9-1中可以看到使用“停止并拷贝”算法的垃圾收集堆的图形描述,这幅图显示了随着 时间推移堆的9次快照。在第一张快照中,堆的下半部分没有被使用,上半部分零散地被对象填 充。堆中包含对象的那部分用灰色斜线表示,快照2显示堆的上半部分逐渐被对象填充,直到如 同快照3所示被填满了。
在这时,垃圾收集器中止程序执行,从根结点开始追踪活动对象图。当遇到活动对象吋就 拷贝到堆的下半部分,每一个都紧挨着上一个被拷贝的对象。这个过程如同快照4所示。
快照5显示了垃圾收集结束后的堆。现在堆的上半部分成了空闲的,下半部分部分填充了活 动对象。快照6显示下半部分逐渐被对象填充,直到它被填满了,如同快照7所示。
9.7按代收集的收集器
简单的停止并拷贝收集器的缺点是,每一次收集时,所有的活动对象都必须被拷贝。大部
分语言的大多数程序都有以下特点,如果我们全面考虑这些,拷贝算法的这个缺点是可以被改 进的。
1)大多数程序创建的大部分对象都具有很短的生命期。
2)大多数程序都创建一些具有非常长生命周期的对象。
简单的拷贝收集器浪费效率的—个主要原因就是,它们每次都把这些生命周期很长的对象来回拷贝,消耗大量的时间。
按代收集的收集器通过把对象按照寿命来分组解决这个效率低下的问题,更多地收集那些 短暂出现的年幼对象,而非寿命较长的对象。在这种方法里,堆被划分成两个或者更多的子堆, 每一个子堆为一 “代”对象服务。最年幼的那一代进行最频繁的垃圾收集。因为大多数对象都 是短促出现的,只有很小部分的年幼对象可以在它们经历第一次收集后还存活。如果一个最年 幼的对象经历了好几次垃圾收集后仍然存活,那么这个对象就成长为寿命更髙的一代:它被转 移到另外一个子堆中去。年龄更高的每一代的收集都没有年轻的那一代来得频繁。每当对象在 它所属的年龄层(代)中变得成熟(逃过了多次垃圾收集)之后,它们就被转移到更高的年龄 层中去。
按代进行的收集技术除了可以应用于拷贝算法,也可以应用于标记并清除算法。不管在哪 种情况下,把堆按照对象年齡层分解都可以提高最基本的垃圾收集算法的性能。
9.8自适应收集器
自适应收集器算法利用了如下事实:在某种情况下某些垃圾收集算法工作得更好,而另外 一些收集算法在另外的淸况下工作得更好。自适应算法监视堆中的情形,并且对应地调整为合 适的垃圾收集技术。在程序调整的时候可能会调整某种简单的垃圾收集算法的参数,也可能快 速转换到另一种不同的算法。或者把堆划分为子堆,同时在不同的子堆中使用不同的算法。
使用自适应方法,Java虚拟机实现的设计者不需要只选择一种特定的垃圾收集算法。可以使 用多种技术,以便在每种技术最搜长的场合使用它们。
达到(或者试图达到)非破坏性垃圾收集的方法是使用渐进式收集算法。渐进式垃圾收集器就是不试图一次性发现并回收所有不可触及的对象,而是每次发现并回收一部分。因为每次 都只有堆的一部分执行垃圾收集,因此理论上说每一次收集会持续更短的时间。如果有一个这 样的支持渐进收集方法的垃圾收集器,每次可以保证(或者至少非常接近)不超过一个最大时 间长度,就可以让Java虚拟机适合实时环境。限时渐进垃圾收集器在用户环境中也令人满意,因 为这样的收集器可以消除用户可察觉得到的垃圾收集停顿。
通常渐进式收集器都是按代收集的收集器,大部分调用中,都是收集堆的一部分。在本聿
的前面部分曾经提到,按代收集的收集器把堆划分为两个或多个年龄层,每一个都拥有自己的 子堆凭经验可知,大部分对象都很快消亡,利用这一点,按代收集的垃圾收集器在年幼的子 堆中比在年长的子堆中活动更频繁。因为除了最高寿的那个年龄层(成熟对象空间)之外,每 一个子堆中都可以给定一个最大尺寸,按代收集的收集器可以大体上保证在一个最大时间值内 渐进地收集所有的对象(最高寿的除外成熟对象空间;没法给定最大尺寸,因为,任何在其他 年龄层中不再适合的对象总要有个去处,它们没有其他地方可去。
火车算法最早是由理查德•哈德森(Richard Hudson )和埃里特.莫斯(Eliot Moss )提出 的,目前正用于Sun公司的Hotspoi虚拟机,该算法详细说明了按代收集的垃圾收集器的成熟对象 空间的组织:火车算法的目的是为了在成熟对象空间提供限定时间的渐进收集。
9.10终结
在Java语言里,一个对象可以拥有终结方法:这个方法是垃圾收集器在释放对象前必须运行。 这个可能存在的终结方法使得任何java虚拟机的垃圾收集器要完成的工作更加复杂。
给一个类加上终结方法,只需简单地在类中声明一个方法:
垃圾收集器必须检查它所发现的不再被引用的对象是否存在finaHze ()方法。
因为,存在终结方法时,Java虚拟机的垃圾收集器必须每次在收集时执行一些额外的步骤。 首先,垃圾收集器必须使用某种方法检测出不再被引用的对象(称作第一遍扫描)。然后,它必 须检查它检测出的不再被引用的对象是否声明了终结方法。如果时间允许的话,可能在这个时 候垃圾收集过程就着手处理这些存在的终结方法。
当执行了所有的终结方法之后,垃圾收集器必须从根结点开始再次检测不再被引用的对象 (称作第二遇扫描)。这个步骤是必要的,因为终结方法可能“复活” 了某些不再被引用的对象, 使它们再次被引用了。最后,垃圾收集器才能释放那些在第一次和第二次扫描中发现的都没有 被引用的对象。
为了减少释放内存的时间,在扫描到某些对象拥有终结方法和运行这些终结方法之间,垃 圾收集器可以有选择地插人一个步骤。一旦垃圾收集器执行了第一遍扫描,并且找到一些不再 被引用的对象需要执行终结,它可以运行一次小型的追踪,从需要执行终结的对象开始(而非 从根结点开始)。任何满足如下条件的对象——从根结点开始不可触及(在第一遍扫描中检测出), 以及从将要被终结的对象开始不可触及——这些对象不可能在执行终结方法时复活,它们可以立即被释放。
如果一个带有终结方法的对象不再被引用,并且它的终结方法运行过了,垃圾收集器必须 使用某种方法记住这一点,而不能再次执行这个对象的终结方法。如果这个对象被它自己的终 结方法或者其他对象的终结方法复活了,稍后再次不再被引用,垃圾收集器必须像对待一个没 有终结方法的对象一样对待它。
使用Java编程时,必须记住一点是垃圾收集器运行对象的终结方法。因为总是无法预测何 时对象会被垃圾收集,所以也无法预测对象的终结方法何时运行。在第2章曾讲过,应该避免编 写这样的程序,即程序的正确性依赖于对象的终结方法所运行的时机。比如,如果不再被引用 的对象的终结方法释放了一个以后程序将会用到的资源,这个资源将直到垃圾执行器运行了这 个对象的终结方法后才能够使用。如果程序在垃圾收集器有机会终结这个不在被引用的对象之 前需要这个资源,这个程序将无法得到该资源。
9.11对象可触及性的生命周期
在版本1.2之前,在垃圾收集器看来。堆中的每一个对象都有三种状态之一:可触及的,可 复活的,以及不可触及的。如果垃圾收集器可以从根节点开始通过追踪“触及“到这个对象,它就是可触及的。每一个对象都是从可触及状态开始它的生命周期的,只要程序还保留至少一 个可以触及的引用到该对象,它就一直保持可触及状态。一旦程序释放了所有到该对象的引用 然后这个对象就变成可复活状态。
如果-个对象满足如下条件,它就处于可复活状态:它在从根节点开始的追踪图中不可触 及,但是有可能在垃圾收集器执行某些终结方法时触及。不仅仅是那些声明了finalize ()方法 的对象,而是所有的对象都要经过可复活状态。前面的部分讲到了,通过再次触及对象,对象 的终结方法可能“复活”对象本身或者其他对象。因为通过对象自己定义的finalize (),或者其他对象的该方法,任何处于可复活状态的对象都可能再次复活,所以垃圾收集器就不能够归还可复活的对象所占据的内存,直到它确信不再有任何终结方法有机会把这个对象复活-在执行所有可复活对象可能声明的finalize ()方法之后,垃圾收集器会把那些处于可复活状态的对象 或者转化为可触及的状态(那些被复活的对象),或者前进到不可触及状态。
不可触及状态标志着不但对象不再被触及,而且也不可能通过任何终结方法复活。不可触 及的对象不再对程序的执行产生影响。可以自由地回收它们所占据的内存。
在版本1.2中,对原来的三个可触及性状态——可触及的,可复活的,不可触及的——扩充了三个新状态:软可触及,弱可触及,以及影子可触及。因为这三个新状态表示了三种新的可 触及性(逐渐减弱),原来在版本1.2之前简单地被称作“可触及”的状态现在从版本1.2开始被称作“强可触及' 从根节点开始的任何直接引用,比如一个局部变量,是强可触及的。同理,任何从强可触及对象的实例变量引用的对象也是强可触及的。
9.11.1引用对象
可触及性的三个比较弱的形式涉及到从版本I.2开始新引入的实体一引用对象:引用对象 封装了指向其他对象的连接:被指向的对象称为引用目标。所有的引用对象都是抽象的 java.lang.ref.Reference类的子类的实例。Reference类家族如图9-3所示,包含了三个直接的子类:SoftRefercnce, WeakReferencc, Phantom Reference。SoftReference 对象封装了对引用目标的 “软引用” ;WeakReference对象封装了对引用目标的“弱引用”;而PhamomReference对象封装 了对引用目标的“影子引用“。强引用和较弱形式的引用——软引用、弱引用和影子引用——之 间最基本的差别是强引用禁止引用目标被垃圾收集。而软引用、弱引用和影子引用不禁止。
要创建一个软引用、弱引用或者影子引用,简单地把强引用传递到对应的引用对象的构造 方法中去。比如,要创建一个对某个Cow对象的软引用,就把一个指向Cow对象的强引用传递到 一个新的SoftReference对象的构造方法中。通过维护对这个SoftReference对象的强引用,也维护 了对这个Cow对象的软引用:
图9-4表示了这样一个SoftRefercnce对象,它封装了对一个Cow对象的软引用。Soft-Reference对象被一个局部变量所强引用,和所有的局部变量一样,对于垃圾收集器来说这是一 个根节点。在前面说过,垃圾收集器的根节点包含的引用和强可触及对象的实例变量包含的引用都是强引用。因为图9-4中所示的SoftReference对象被一个强引用所引用,这个SoftReference对象是强可触及的。假设只有这个SoftReference对象才拥有对此Cow对象的引用,那么这个Cow 对象是软可触及的。因为垃圾收集器从根节点开始只有通过一个软引用才能触及到此Cow对象。
-一旦一个引用对象创建后,它将一直维持到它的引用目标的软引用、弱引用或者影子引用, 直到它被程序或者垃圾收集器清除。要清除一个引用对象,程序或者垃圾收集器只需要调用引 用对象的clear ()方法,这个方法是Reference类定义的。通过清除引用对象,就切断了引用对象的软引用、弱引用或者影子引用。比如,如果程序或者垃圾收集器试图运行图9-4中的 SoftRefcrence对象的clear ()方法,通往此Cow对象的软连接就被切断了,这个Cow对象就不再 是软可触及的了。
9.11.2可触及性状态的变化
前面讲到过,引用对象的目的是为了能够指向某些对象,这些对象仍然随时可以被垃圾收 集器收集。换个说法就是,垃圾收集器可以随意更改不是强可触及的任何对象的可触及性状态。 当使用软引用、弱引用或者影子引用的时候,因为跟踪垃圾收集器改变对象的可触及性状态常 常很重要,因此可以安排在这种变化发生时得到一个通知。如果对可触及性状态的改变有兴趣, 可以把引用对象和引用队列关联起来。引用队列是java.lang.ref.ReferenceQueue类的一个实例: 垃圾收集器在改变可触及性状态时会添加(编人队列)所涉及的引用对象。设置并且观察引用队列,当垃圾收集器是对你感兴趣的对象改变可触及性状态时,你就可以异步得到通知了。
要把一个引用对象和一个引用队列关联起来,可以简单地在创建引用对象时把引用作为构 造方法的参数传递到引用队列。如此创建的引用对象除了保持对引用目标的引用外,还保持对 引用队列的引用。当垃圾收集器对引用目标的可触及性状态做了改变时,它就会把引用对象加 入到与它有关联的引用队列中去。比如,图9-5中所示的WeakReference对象创建的时候,有两个 引用被传递到构造方法:一个指向Fox对象的引用和一个指向ReferenceQueue对象的引用。当垃圾收集器决定收集弱可触及的Fox对象的时候,它会清除WeakReference对象(执行WeakReference 的 clear ( )方法),可能立即就把这个WeakReference对象加人到它的引用队列中也可能在稍后的某个时间加入。
为了把引用对象加入到它所关联的队列中,垃圾收集器执行它的enqucue()方法。enqueue() 方法是在超类Reference中定义的,只有在创建引用对象时关联了一个队列、并且仅当该对象的 enqueue ()方法第一次执行时,才把引用对象加入到这个队列中。程序可以有两种方法来监控 引用队列:用poll()方法拉出来,或者用remove ()方法阻塞它。如果在队列中有一个引用对象在等待,不管在队列上执行poll()还是remove ()方法,都会取走这个对象,返回给调用者。 如果没有引用对象在等待,那么poll()会立即返回null,而remove ()会阻塞,直到有一个引用对象加人队列。一旦有对象到达队列,remove()会取走并且返回它。
在不同的情况下,垃圾收集器把软引用、弱引用和影子引用对象加入队列表示三种不同的 可触及性状态的转换。这表示六种可触及状态,状态变化的详情如下所列:
•强可触及对象可以从根节点不通过任何引用对象搜索到。对象生命周期从强可触及状态开始,并且只要有根节点或者另外一个强可触及对象引用它,就保持强可触及状态。垃圾收集器不会试图回收强可触及对象占据的内存空间。
•软可触及对象不是强可触及的,但是可以从根节点开始通过一个或多个(未被清除的) 软引用对象触及。垃圾收集器可能回收软可触及的对象所占据的内存。如果这发生了,它会清除所有到此软可触及对象的软引用。当垃圾收集器清除一个和引用队列有关联的软引 用对象时,它把该软引用对象加入队列。
•弱可触及对象既不是强可触及的也不是软可触及的,但是从根节点开始可以通过—个或 多个(未被清除的)弱引用对象触及。垃圾收集器必须归还弱可触及对象所占据的内存。 这发生的时候,它会淸除所有到此弱可触及对象的弱引用。当垃圾收集器清除一个和引用 队列有关联的弱引用对象时,它把该弱引用对象加入队列。
•可复活的对象既不是强可触及、软可触及,也不是弱可触及,值是仍然可能通过执行某 些终结方法复活到这几种状态之一。
•影子可触及对象不是强可触及,软可触及,也不是弱可触及,并已经被断定不会被任何终结方法复活(如果它自己定义了终结方法,它的终结方法已经被运行过了),并且它可 以从根节点开始通过一个或多个(未被清除的)影子引用对象触及。一旦某个被影子引用 的对象变成影子可触及状态,垃圾收集器立即把该引用对象加入队列。垃圾收集器从不会 清除一个影子引用,所有的影子引用都必须由程序明确地清除。(自我感言:影子引用是为了跟踪不可触及对象,影子引用=不可复活对象+虚拟引用)
•不可触及 一个对象不是强可触及、软可触及、弱可触及,也不是影子可触及,并且它不 可复活。不可触及的对象已经准备好被回收了。
请注意,垃圾收集器在把软引用对象和弱引用对象加入队列的时候,是在它们的引用目标离开相应的可触及状态时;而把影子引用对象加入队列是在引用目标进人相应状态时。在垃圾收集器清除引用对象时也有差别,软引用对象和弱引用对象在加人队列之前得到了清除,而影子引用对象却不会。这就是说,垃圾收集器把软引用对象加入队列标志着它的引用对象刚刚离开软可触及状态;(自我感言:应该是标志着它的引用目标刚刚离开软可触及状态);同样,垃圾收集器把弱引用对象加人队列标志着它的引用对象刚刚离开弱可触及状态(自我感言:应该是标志着它的引用目标刚刚离开弱可触及状态);但是垃圾收集器把影子引用对象加入队列标志着引用目标已经进人了影子可触及状 态。影子可触及对象会保持影子可触及状态,直到程序显式地淸除了引用对象。(自我感言:如果此时引用目标是影子可触及状态,程序显式地淸除了所有影子引用对象,则该引用目标的状态变成不可触及状态)
9.11.3缓存、规范映射和临终清理
垃圾收集器对待软、弱和影子对象的方法不同,因为每一种都是被设计为,为程序提供不 同的服务。软引用使你可以创建内存中的缓存,它与程序的整体内存需求有关。弱引用使你可 以创建规范映射,比如哈希表,它的关键字和值在没有其他程序部分的引用时可以从映射中清 除。影子引用使你可以实现除终结方法以外的更加复杂的临终清理政策。
要使用一个软引用或者弱引用的引用目标,可以调用引用对象的get()方法。如果引用目标还没有被清除,则会得到对引用目标的一个强引用,就可以用通常的方法去使用它了。如果引 用目标已经被清除了,则会得到null。如果调用影子引用对象的get ()方法,那么无论如何只能得到null,即使引用对象还没有被清除。因为影子可触及状态只有经过可复活状态之后才能获得, 一个影子引用对象没有提供任何方法来访问它的引用目标。调用影子引用对象的get ()方法只能得到null,即使影子引用还没有清除。因为,如果它返回一个影子可触及对象的强引用,实际上它就复活了这个对象。这就是说,如果一个对象到达了影子可触及状态,它不能再被复活。
虚拟机的实现需要在抛出OutOfMemoryError之前清除软引用,但在其他情况下可以自行选择清理的时间或是否清除它们。实现最好是只在内存不敷所需时才去清除软连接,清除的时候 先清除老的而不是新的,清除长期未用的而不是最近刚刚用过的。
软引用可以让你在内存中缓存那些需要从外部数据源费时取回的数据,比如文件中、数据 库里或者网络上的数据。所以只要虚拟机有足够的内存,可以在堆中保存所有的强引用的数据 以及软引用的数据,软引用大体上对于在堆中保存软引用的数据已经足够强了。如果内存变得 紧张,垃圾收集器会决定淸除软引用,回收被软引用的数据所占用的空间。下一次程序需要使 用这个数据的时候,可能不得不再次从外部数据源装入。同时,虚拟机就有更多的空间用来调 整程序强引用(或者其他软引用)需要的内存。
弱引用类似于软引用,不同的是:垃圾收集器可以自行决定是否清除指向软可触及的对象的软连接,而它必须在判断出对象处于弱连接状态时就立即清除弱引用。弱引用使得你可以用关键字和值来创建规范映射。java.util.WeakHashMap类就是用弱引用提供这样的规范映射。可以通过put()方法加人键-值对到WeakHashMap的实例,如同可以对实现了java.util.Map的任何 类的实例所做的那样。但是在WeakHashMap中,关键字对象是通过一个关联到引用队列的弱引 用实现的。如果垃圾收集器断定某个关键字对象是弱可触及的,它会清除引用并且把任何弱引 用到该对象的引用加入各自的队列。下一次当WeakHashMap被访问的时候,它从引用队列里面 拉出所有垃圾收集器放在那儿的弱引用对象。WeakHashMap就会清除它的映射中任何关键字属 于队列中弱引用的键-值对。这就是说,如果把一个键-值对加人到WeakHashMap,在程序显式 地使用remove ()方法移出它或者垃圾收集器发现关键字对象是弱可触及之前,它会一直保留 在 WeakHashMap 里面。
影子可触及性表示对象即将被回收。当垃圾收集器断定影子引用对象的引用目标处于影子 可触及状态时,它把该影子引用加人到所关联的引用队列。(和软引用对象、弱引用对象不同, 软引用对象和弱引用对象可以在创建时选择不和一个引用队列关联,而影子引用对象没有一个 关联的引用队列就无法创建实例。)可以利用引用队列中影子引用的到达来触发一些你希望在对 象生命周期的最后时刻需要完成的动作。因为无法获得对影子可触及对象的强引用(get ()方 法总是返回null),所以无法完成那些需要访问影子目标的实例变量的动作。在完成了影子可触及对象的临终淸理之后,必须调用指向它的影子引用对象的Clear ()方法。调用一个影子引用 对象的clear ()方法是对它的引用对象的致命一击,把引用目标从影子可触及状态导向它的终 点:不可触及状态。