Java 垃圾收集(Garbage Collection)

原文:http://www.artima.com/insidejvm/ed2/gcP.html

垃圾收集 ----Bill Venners

          Java虚拟机堆栈中存储运行中的java应用程序创建的所有对象。这些对象在代码里由new,newarray,anewarray和multianewarray命令创建,但是代码里从不显示的释放这些创建的对象。Garbage Collection(gc程序)是一个运行于jvm上用来自动释放那些不再被java运行程序引用的对象的后台线程。

为什么需要GC

“garbage collection”(垃圾收集)表示那些不再被运行的程序所使用的对象(jvm里的垃圾)是应该被丢弃的。一种更准确更现代的说法应该叫“内存回收”。如果一个对象不再被程序使用,那么这个对象所占用的内存空间就应该被回收,会收回后得到的内存可以被后续程序创建新对象时使用。Java垃圾回收器要判断堆栈中哪些对象是不再被程序使用的,并回收这些对象所占用的空间,留个程序后续使用。在回收不再被使用的对象的进程中,垃圾收集器要运行每一个正在被回收的对象的finalize方法。


另外垃圾收集时,垃圾回收器还要处理堆栈内存碎片问题。应用程序运行的过程中都会产生内存碎片,在一块内存中新的对象被创建,同时不再使用的对象被回收,这样被回收的内存就会夹杂在仍然被程序使用的对象所占用的内存中间,这样就产生了垃圾碎片。在堆栈中即使空闲的内存空间的总的大小足够大,在为新的对象分配内存是我们有时候还是需要使用扩展的空间。当内存中空闲的空间都不是连续的并且这些分散的空闲内存中找不到适合新对象的空间时就会出现这种情况。在内存系统中,如果不断增长的堆中如果需要不断增加额外的内存页,那么程序的性能就会下降。在内存有限的嵌入式系统中,内存碎片会导致不必要的程序内存不足(run out of memory)异常。


垃圾收集的第二个好处是保证程序的完整性,它是java安全策略的重要组成部分,java程序员不会因为错误的内存释放操作导致程序崩溃。


java垃圾收集的一个潜在的问题是它会影响程序的性能。JVM要在程序运行时跟踪内存中被程序使用的对象,并找出不再被程序使用的对象进行内存回收,这些操作都会耗费cpu的时间,另外java程序员在垃圾收集环境下对cpu何时回收内存的控制权比较小。


垃圾收集算法

任何垃圾收集器都需要做两件基本的事:1)检测出哪些对象是不再被程序使用的对象 2)回收垃圾对象占用的内存空间,并使这些空间可以被程序后续使用。


垃圾对象检测是通过定义一组对象的根引用的集合,判断是否可以通过根找到对象。任何一个对象,如果根引用中有路径可以寻找到那个对象,那么我们称这个对象时活着的,否则就认为这个对象死掉了,也就是垃圾对象,因为一旦失去了一个对象的所有引用,那么这个对象就不可能再被程序用到,它也就不可能对程序的后续执行产生任何影响。


对象根引用集合是由具体实现决定的,但是它一定会包含以下项:所有局部变量的引用;所有栈帧的操作数栈(operand stack of any stack frame);所有类的对象引用。另一个跟引用集合的来源是所有已加载的类的常量池中的所有对象的引用,比如string对象。这些常量池对象可能包含存储在堆上的string对象,比如类名,超类名,接口名,属性字段的名字,方法名,签名的名字和方法的签名等。根对象引用集合还可能包括被传递给本地方法(native method)的尚未被本地方法释放的对象引用(这取决于本地方法接口,本地方法可能可以释放传递进来的对象引用,比如通过方法返回释放,通过显示的回调释放,或者是以上两种方法的结合来释放),任何从垃圾回收队分配的JVM运行时数据域都是潜在的根引用集合组成部分。例如在某些方法实现中,方法内的对象可能是在垃圾回收堆上分配的,这样可以允许同一个垃圾收集器检测并释放垃圾对象。


任何被根对象引用集合引用的对象都是可达的(可以通过跟引用在内存中找到该对象),所以这些对象都是“活着的”,另外被任何活着的对象引用的对象也是可达对象,程序还可以在后续的执行中访问这些对象,所以这样的对象必须在堆栈中保留,而不可达对象是程序后续执行中无法再访问到的对象,所以是应该被回收的。


JVM是可以被实现的,这样垃圾回收器可以知道真正的对象引用和看起来像对象引用的原生类型(比如int型,如果int型的变量被解释为一个本地指针,那么它可能指向堆内存中的某个对象)之间的差别。然而有些垃圾回收器选择不区分真正的对象引用和看起来像引用的原生类型,这样的回收器被称为保守的回收器,他们有时候不会释放所有的不再被使用的对象。有些时候垃圾对象可能被保守的垃圾回收器误认为任然“活着”,因为有时候一个看似是对象引用的原生类型仍指向它。保守垃圾回收器在回收准确性上的损失却一定程度上赢得了垃圾回收速度上的优势。


两种基本的检测垃圾对象的方法是引用计数和跟踪。引用计数垃圾回收器通过维持一个堆对象的计数器来判断对象是否还“活着”,计数器记录目前指向某个对象的引用有多少个。跟踪回收器通过从根节点到实际对象的跟踪图来跟踪一个对象,如果在跟踪的路劲上遇到了对象就以某种方式对对象进行标记,那些没有被标记的对象就是“不可达”的对象,它们应该被垃圾回收器回收。


引用计数回收器

引用计数是一种早期的垃圾回收策略,这种方法在堆栈上为每个对象维持一个引用计数值,当创建一个对象并将该对象的引用赋值给一个变量的时候,这个引用计数的值就被初始化为1,当这个对象的引用被赋值给其他变量的时候,计数值也相应的增加,任何一个引用计数值为0的对象都是垃圾对象,应该被垃圾回收器回收。当一个对象A被回收时,任何一个被它引用的对象(B,C,D...)的引用计数值都应该减一,这可能使得某些(例如B)被它引用的对象也被回收,因为对象A可能是最后一个持有B的引用的对象。

引用计数法的一个好处是计数回收器可以在很小的时间内完成垃圾收集,这样不会长时间的打断正在运行的主程序。这一特性使得它很适合实时系统(real-time environment),因为实时系统程序不允许被长时间打断。引用计数的一个缺点是它无法检测循环引用,比如两个或者两个以上的对象之间相互引用(如:A持有B的引用,B持有C的引用,C又持有A的引用)。这样的对象的引用计数值永远不会为0,即使他从根引用集合中已经没有可以找到他们的路径了。引用计数的另一个缺点是它需要一定的性能消耗花在引用计数值的增减上。由于引用计数法的这一固有的缺点,这一技术在现在已经很少使用了。现实使用中你更可能遇到的是跟踪回收法。


跟踪回收器

跟踪回收器从根对象引用集合开始为每个对象生成一个“跟踪图”,当垃圾回收器从根对象引用集合中按照某一路径找到某个对象时,就以一种特定的方式对对象进行标记。这种标记方法可以是通过在对象本身上设置一个标签,也可以通过是用单独的位图来记录。当完成了对所有对象的标记后,那些没有标记的对象就是垃圾对象,它们应该被回收器回收。有一个基本的跟踪算法被称作“标记清扫法”,这个名字表明算法分为标记和清扫两个阶段。在标记阶段,回收器从根对象引用集合开始遍历所有路径树并标记遍历过程中遇到的所有对象。在清扫阶段,未被标记的对象被释放,所得到的内存留作程序后续使用。在清扫阶段JVM必须包含每个对象的终结(隐式调用对象的finalize方法)。

压缩回收器

JVM的垃圾回收器会希望拥有一种解决内存碎片的方法,“标记清扫”垃圾回收器常用的两种方法是压缩和拷贝。这两种方法都在程序运行的过程中通过移动内存中的对象来解决内存碎片问题。压缩回收器将堆栈中任然活着的对象移动到堆的一端,同时将所有对活着的对象的引用更新为对堆中新位置的对象的引用,这一操作完成后堆的另一端将会是大块的连续空闲内存。

更新被移动的对象的引用有时候只是简单的加一层间接引用。新的对象引用指向一个对象句柄表而不是直接直接引用堆栈上的对象。当一个对象被移动后只需要简单的更新对象handler表的对象句柄指向新的内存位置(object handler),而运行中的程序的对象引用任然指向句柄表中的句柄----这些句柄是不会被移动的。这种方法可以简化整理内存碎片的工作,但也增加了性能的开销。

拷贝回收器

拷贝回收器将所有活着的对象移动到新的内存位置,在新位置对象被一个接一个的存放在相邻的位置,这样可以避免原来存储对象的旧的内存位置中的空闲内存将对象分割开,拷贝后原来久的内存位置就都是空闲的内存空间了。拷贝回收的好处是对象可以轻易的根据根遍历树拷贝出来,省去了标记和清扫的过程。对象在程序运行的过程中被拷贝到新的位置,之前对象引用仍然留在原来的位置上,这些引用可以让回收器轻易的检测到那些对象是被移动过的,这样垃圾回收器也可以轻易的将原对象的引用更新为原来对象的拷贝的引用。

一个常见的的拷贝回收算法是“停止-复制”算法,这种方式下,堆栈被划分为两个区域,其中的一个区域(可能先是A区域然后是B区域)允许程序在任何时候使用。程序在其中的一个区域A中为对象分配内存,直到该区域的内存被耗尽,这个时候JVM停止正在执行的应用程序,然后遍历堆栈,将已耗尽的内存区的所有活着的对象拷贝到另一个内存区B,当拷贝结束后应用程序恢复执行,之前被耗尽的那个内存区现在被认为是空闲内存区,然后程序在B区域上新建对象,直到它被耗尽,然后重复上述停止-复制过程,只不过这次是将B中活着的对象拷贝到A区域。停止-拷贝回收法需要的内存空间的大小是分配给程序的堆栈大小的两倍,因为在程序运行的过程中只有一半的内存可以被程序使用。在下图中你可以看到停止-拷贝回收法的图解




这幅图按时间顺序展示了堆栈上的9个内存快照。在第一个快照中,内存的低地址那一半是没有被程序使用的空间,而高地址的一半是被程序使用中的内存空间,其中部分已经被程序分配的对象占用(用阴影标出),第二幅快照显示内存逐渐被新分配的对象占用,直到完全被占用,如第三幅内存快照所示。这个时候垃圾回收器会停止程序的执行,并遍历对象跟踪树,并将跟踪到的活着的对象拷贝到低地址那一半的空间中,对象被一个紧邻另一个的顺序放到低地址的那一半内存中,如快照4所示。快照5显示了垃圾回收完成后的内存状况,这个时候高地址那一半内存空间就是空闲内存,而低地址那一半的部分被仍活着的对象占用,快照6显示低地址那一半逐渐被程序分配的新对象占用的情况,直到完全被耗尽,如快照7所示,然后垃圾回收器会再次停止应用程序,接着将低地址那一半中仍活着的对象拷贝到高地址那一半的空间中一个接一个的紧邻存放(快照8),最后回复应用程序的执行,此时低地址的那一半空间又变成了空闲的内存。以后的回收过程会重复上述的过程。

分代回收器

停止-拷贝收集方法的一个缺点是每次都需要拷贝所有的活着的的对象。对于这一缺点,我们可以基于现实程序中常见的两个现象进行改进

1:大部分被程序创建的对象只有很短的生命期

2:大部分程序会创建一些生命期很长的对象,影响停止-拷贝回收法效率的一个原因是回收器会一次又一次的复制生命期很长的那些对象

分代回收器通过按对象的年龄(从被创建到目前这个时刻的时间长短)对对象进行分组来解决这个问题,回收器回收年轻对象的频率会更高。使用这种方法的时候堆栈会被划分为两个活着多个子堆栈,每个子堆栈服务于一代对象,最年轻的那一代对象被回收的频率最大,如果一个对象经历几轮回收之后还活着,那么它会被放入下一代对象的堆栈中,各子堆栈中的对象一代比一代老,他们的被回收的频率也随着对象变老而降低。标记清扫回收和拷贝回收都可以使用分代技术。

适应性回收器

自适应回收算法充分利用在不同的场景下回收算法的效率不同这一特性,它实时监控堆栈的情况,并根据目前的堆栈情况自行选择合适的回收算法,它可能只是简单的调整单个回收算法所使用的参数,也可能是从一个回收算法切换为另一种,还可能是将堆栈划分为多个子堆栈并在不同的子堆栈上分别使用不同的回收算法。

使用自适应回收算法使得jvm的设计者不需要在多种回收算法中进行选择,而只需将多种算法都应用到jvm上,让jvm自行做出选择。

火车算法

同手动释放相比垃圾回收的一个潜在的缺点是程序员对回收垃圾使用的cpu时间的控制权变小了。一般垃圾回收器何时开始回收垃圾,回收所使用的时间有多长这些都是无法预测的。由于垃圾回收器在执行垃圾回收的时候经常停止整个应用程序,因此它可能导致应用程序在随机的时刻停止程度随机的时间,垃圾回收暂停程序也会使得实时应用程序无法及时的响应请求,而及时响应式实时

系统最根本的特性。如果垃圾回收算法可以使得暂停的时间长到能够被使用者察觉或者使得程序不再适合实时系统那么我们称这样的算法为打断性的,为了最小化这一缺点,一个设计垃圾回收器的基本目标就是尽量最小化这种打断性的特性甚至是完全消除它。


一种达到无中断回收的方法是增量回收,增量回收器不是每次回收都去检测并回收所有的垃圾对象,而只是检测回收部分垃圾对象。因为每次都只是检测回收堆栈上的一部分垃圾,理论上垃圾回收的时间也就会短很多,一个应用增量回收法的垃圾回收器可以保证每次垃圾回收使用的时间不会大于某个固定值,这使得它可以被应用于实时系统中。这样的回收器在普通的用户环境中也是很受青睐的,因为他可以消除垃圾收集所带来的用户可以察觉的长时间程序停顿。

火车回收算法最早由Richard Hudson 和 Eliot Moss提出,现在被用于Sun Hotspot virtual machine中,它为分代回收器指定了一种成熟对象空间的组织方法。火车算法的目的是提供一种时间相关的成熟对象空间的增量回收。


车厢,火车和一个火车站(直译)
火车算法在内存空间中将成熟对象空间划分成固定大小的子空间块,而这每个子空间都是每次垃圾回收器被调用时分开回收的(一次只回收其中一个子空间中的垃圾)。火车算法的名字来源于组织成熟对象子空间块的方法。每一个子空间快属于一个集合,集合内的子空间块是有序的,而且所有集合自身也是有序的。这一算法的两位作者为了解释他们的算法,将这些子空间块称为”cars“,子空间块组成的集合称为”trains“。在这一比喻中,成熟对象空间扮演火车站的角色。在同一个集合中的子空间块是有序的,就像在在同一辆火车上的所有车厢是有序的一样。而这些子空间块组成的集合也是有序的,就像火车站的火车也是按照轨道号排序的一样。
火车(车厢集合)按照他们的创建顺序被赋予编号。所以火车站内,第一辆到达的火车进入1号轨道,被编号为1号火车,下一辆进站的火车进入2号轨道,被编号为2号火车,再下一辆进入3号轨道,编号为3号火车如此类推。在这种编号方式下,数字越小就代表火车进站的时间越早(对象越老)。同一辆火车上每次车厢(子空间块)都是加在车尾上最后一节车厢之后,第一个加在火车上的车厢编号为1,下一个编号为2,在下一个编号为3以此类推。在同一火车上编号越小的车厢表明车厢越早被加到火车上(子空间块越老),这一编号管理方式就使得所有的成熟对象空间中的子空间块都是有序的。


图9-2展示了三辆火车,编号分别为1,2,3,1号火车有四节车厢,编号为1.1-1.4,2号有三节车厢编号为2.1-2.3,3号有五节车厢编号为3.1-3.5.车厢1.1在车厢1.2之前,1.2在1.3之前,1号火车的最后一节车厢1.4在2号火车的第一节车厢2.1之前,同样的2.3在3.1之前,每次调用火车回收算法都会回收一个子堆栈块的空间,且只收集编号最小的那一个子堆栈块的空间。因此,第一次运行火车回收算的时候,会回收图9-2中编号为1.1的子堆栈块,下一次则会收集1.2,当编号为1的火车的所有子堆栈块全部收集完后再次运行火车回收算法则会去收集编号为2.1的子堆栈块,以此类推。

当年轻代堆栈空间的对象变老而没有被回收的时候它会被放入成熟对象堆栈空间中。无论何时变老的对象被放入成熟对象堆栈空间的时候,它不会被放入编号最小的“火车”上,而是会放在其他已经存在的“火车”上,或者新建一个或者多个“火车”来存放他们。

回收车厢

每次调用火车回收算法的时候,垃圾回收器会回收编号最小的火车的最小编号“车厢”或者回收编号最小的整辆“火车”。算法首先检查所有指向编号最小的火车的所有车厢的对象的引用,如果没有外部的引用指向其中的任何“车厢”,那么整列“火车”都会被回收。这一步骤使得火车算法可以回收那些无法放入单个子堆栈块的大块循环引用数据结构对象,因为在下一个步骤中这些很大的循环引用数据结构对象必须被保证要被回收。


如果编号最小的“火车”被检测出来全是垃圾,垃圾回收器会回收所有被“火车”对象占用的空间,如果这辆“火车”上并不是全是垃圾对象,那么算法会将注意力转向编号最小的那节“车厢”,处理时,算法会移动或者释放一些“车厢”上的对象,算法开始会将编号最小的这节“车厢”上尚有外部引用的对象移动到其他车厢,经过这个处理,车厢上剩下的对象就都是可以被回收的垃圾了,这个时候算法会回收这节“车厢”占用的所有空间(释放那些没有外部引用的对象的操作仍然是在编号最小的那节“车厢”上执行的)。

确保循环引用数据结构对象在同一辆“火车”上被回收的关键是算法如何移动对象。如果一个对象在正在被回收的“车厢”上,且在成熟对象堆栈空间之外存在对它的引用,那么这个对象会被移动到其他没有被回收的“车厢”上。如果一个对象在成熟堆栈空间内被其他“火车”上的对象引用,那么这个对象会被移动到引用它的那辆“火车”上,然后回到正在被回收的“火车”上继续扫描寻找被这个被移动的对象引用的对象,任何被它引用的对象都会被移动到引用他们的那辆“火车”上。·然后同样也会扫描被回收的火车上是否有新的被这些被移动的对象引用的对象,如果有也将他们移动到引用他们的“火车”上,递归的执行这个过程,直到没有新的被引用的对象出现为止。如果在这个过程中用来容纳被移动的对象的“车厢”满了,那么算法会新建一个“车厢”来容纳他们,并将这个车厢加在火车的车尾。

一旦在成熟对象空间外和成熟对象空间内的“火车”上不再存在对正在被回收的“车厢”上的对象的引用,那么我们可以知道其他任何引用对正在被回收的“车厢”上的对象的对象都来自于同一辆火车的其他“车厢”。这时候算法会将这些对象移动到与这个正在被回收的“车厢”在同一编号最小的火车上的最后一个“车厢”上,然后回到正在被回收的“车厢”上寻找被这些移动的对象引用的新对象,如果找到就也将它们移动到引用他们的那节车厢上,重复这个过程直到没有新的被引用的对象出现为止,然后算法会回收被编号最小的那节车厢占用的所有空间。

因此火车算法每次调用的时候都回收编号最小的那辆“火车”上编号最小的那节”车厢“活着回收编号最小的那一整辆“火车”。火车中很重要的一点是:即使循环引用数据结构对像非常大,大到单个的子堆栈块(车厢)都无法容纳,算法也必须保证它们最终会被回收。因为对象会被移动到引用它们的“火车”上,所以相关联的对象可能会被集中在一起。最终所有的循环引用数据结构对象都会被回收,无论它们有多大。

记忆集合和活跃的对象们

正像之前提到的,火车回收算法是为了提供时间相关的成熟对象空间增量回收。由于可以指定子堆栈块最大值并且每次调用回收器值回收一个子堆栈块,火车回收算法大部分时候可以保证每次调用回收器进行回收所消耗的时间小于回收单个子堆栈块的最大时间。然而不幸的是火车算法无法保证每次都是如此,因为算法要做的事远不止拷贝对象。

为了加速来及回收的速度,火车算法利用了记忆集合,记忆集合是一个存放所有在“火车”外或”车厢“外但引用”火车“或”车厢“内的对象的对象,火车算法在成熟对象空间为每一个”车厢“和没一辆”火车“维护一个这样的记忆集合。为特定的”车厢“准备的记忆集合中包含引用这个车厢对象的对象集合,如果一个记忆集合为空,这说明与之对应的那个车厢上的所有对象都没有外部引用,而这些对象就是可以被垃圾回收器回收的对象。

记忆集合是一个用来提高火车回收算法效率的工具。如果发现一辆”火车“的某个车厢有空的记忆集合,那么我们就知道这节车厢上全是垃圾,那么它占用的空间可以立即被回收。同样如果一辆”火车“的记忆集合是空的,那么这辆”火车“也全部是垃圾,那么它占用的全部空间也可以立刻被回收。当火车算法将一个对象移动到别的”车厢“活着”火车“上的时候,记忆集合中的信息可以帮助算法快速的更新这些对象的引用到新的内存位置。

尽管火车算法在一次调用中能拷贝的字节量受子堆栈块的大小的限制,但是移动一个活跃对象(有很对外部引用的对象)的工作量却是不会受到限制的。每次算法移动一个对象时,它必须遍历这个对象所在的记忆集合并且更新所有指向这个对象的引用到最新的内存位置。由于一个对象的引用的数量是没有限制的,因此更新这个对象的引用所需的时间也是没有限制的。所以在某些情况下火车算法还是会被中断的。然而,尽管会因为活跃对象使回收过程被打断,火车算法在很多时候还是可以很好的工作。

对象终结方法

在java总,对象可能会有一个对象终结器:在垃圾回收器释放一个对象前必须调用的方法。潜在的终结方法会增加JVM垃圾回收工作的复杂性,因为垃圾回收器在释放对象前必须检查每一个将要被回收的垃圾对象是否拥有finalize()方法。

因为finalize()方法,垃圾回收器在回收垃圾时必须要多执行几个步骤的操作。首先,垃圾回收器必须检测出所有没有引用的垃圾对象,然后垃圾回收器要检查这些对象中是否有对象拥有finalize()方法,如果有足够的时间,在这个时候(对象被释放前)要为那些拥有finalize方法的对象调用该方法。

在执行完所有对象的finalize方法后,垃圾回收器有必须从跟引用集合开始检测没有被引用的对象,为什么需要这个工作呢?那是因为对象的finalize方法可能会使对象复活(使对象再次被其他对象引用),最后垃圾回收器才可以释放它检测出来的垃圾对象。

为了减少回收所消耗的时间,垃圾回收器可以有选择的在检测垃圾对象和执行垃圾对象的finalize方法中间插入一个中间步骤:从等带被执行finalize方法的对象开始检测是否有对象会复活,任何一个从根节点不可达的且在finalize方法中无法复活的对象都是可以被立即回收的对象。

如果一个有finalize方法的对象没有被外部引用,并且它的finalize方法已经执行过了,那么垃圾回收器就必须保证它的finalize方法不会被再次执行。如果一个对象在自己的或者其他对象的finalize方法执行中被复活了,然后又变成了未被引用的,那么垃圾回收器必须像对待没有finalize方法的对象一样对待它。

在你写java程序的时候,你必须时刻记住运行对象finalize放的是垃圾回收器,因为我们无法准确的预测一个垃圾对象会在何时被垃圾回收器回收,所以我们也就无法预测对象的finalize方法何时会被执行。你应该避免写出正确性依赖于对象finalize方法的代码。例如一个垃圾对象A的finalize方法释放了一个资源,而这个资源在后面又是程序需要的。这个资源本在A的finalize方法执行完成之前变成了不可达的。假如程序在垃圾回收器运行A的finalize方法之前需要这个资源的话,那么程序就可能因为这个资源仍被别的对象占用而出现问题。

对象的可达生命周期

在(JVM)1.2版本以前,从垃圾回收器的角度来看,每一个堆栈上的对象都处于三种状态中的一种:可达的,可复活的,不可达的。如果垃圾回收器可以从根引用集合跟踪到某个对象那么这个对象就是可达的。每一个对象的生命期都是从可达状态开始的,这个状态一直维持到没有任何外部引用为止。当垃圾回收器释放对了某个对象的所有引用,这个对象就变成了可以复活的。


如果垃圾回收器暂时无法从根引用集合跟踪到某个对象,但是后面这个对象又可能因为它的finalize方法被运行而变得可达,那么这个对象就是可复活的,所有的对象都会经历可复活状态期,而不仅仅是那些声明了finalize方法的对象。在之前已经提到,一个对象的finalize方法可能是它自己复活,也可能使得其他的对象复活,所以垃圾回收器在确定一个对象不可能被复活之前不可以回收处于可复活状态的对象占用的内存空间。通过运行对象的finalize方法,垃圾回收器会转变所有可复活状态的对象,可能从可复活变为不可达,也可能从可复活变为可达。


如果一个对象处于不可达状态,那么这个对象不可能再次被程序用到也不可能因为它自己或者别的对象的finalize方法的调用而复活,以后它不会对运行的程序产生任何影响,因此垃圾回收器可以毫无顾忌的回收它占用的内存。

在1.2版本的jvm中,这三种原始的状态----可达,可复活,不可达------变成了三种新的状态softly可达,weakly可达,phantom可达,这三种新的状态相对于1.2版本之前的strongly可达都表示对象某种程度上的可达性,任何直接直接被根引用集合引用的对象都是strongly可达的,例如局部变量,同样任何一个被strongly可达的对象引用的对象也是strongly可达的。


引用对象

弱(weakly)可达形式的对象都和之前介绍的1.2版本中的“引用对象”有关,一个“引用对象“封装了一个指向其他对象的引用,被称为引用者。所有的”引用对象“都是java抽象类java.lang.ref.Reference的子类的一个实例。Reference类家族包含三个子类:SoftReference,WeakReference和PhantomReference如下图所示。一个SoftReference对象封装了一个指向引用者的”软引用(soft reference)“,一个WeakReference对象封装了一个指向引用者的”弱引用(weak reference)“,一个PhantomReference对象封装了一个指向引用者的”虚位引用(phantom reference)“。强引用与他的三个减弱的引用兄弟之间的根本区别在于强引用会阻止垃圾回收器回收引用者,而其他三个不会阻止垃圾回收器这么做。


如果要创建soft、weak活着phantom引用你只需要将一个强引用作为参数传递给这三个对象的合适的构造器就可以了。例如创建一个指向Cow对象的软引用,你就给SoftReference构造器传递一个指向Cow对象的强引用就可以了。你维持一个指向SoftReference对象的强引用的同时也就维持了一个指向Cow对象的软引用。下图就展示了一个封装有Cow对象软引用的软引用对象。



图示中,SoftReference对象被一个局部变量强引用,这个局部变量就像其他局部变量一样被作为跟引用集合的一个对象节点,之前已经说过根引用集合中的对象和被强引用对象引用的对象都是强引用对象。由于图中的SoftReference对象被一个强引用对象引用,所以SoftReference对象也是Strongly可达的。假如SoftReference对象只包含了指向Cow对象的一个引用,那么Cow对象就是弱引用的。这是因为垃圾回收器只能通过一个弱引用找到这个Cow对象。

一旦一个”引用对象“被创建,它就会继续持有引用者的软引用,弱引用和虚位引用直到它被程序活着垃圾回收器清除。要清除一个引用对象程序和垃圾回收器只需要调用它的clear方法就可以了。清除掉引用对象会使得它持有的软引用,弱应用和虚位引用全部失效。例如如果一个程序或者垃圾回收器调用了图示中的SoftReference对象的clear方法,那么指向Cow对象的软引用就会失效,那么Cow对象就再也不是softly可达的了。


可达状态变换

就像之前提到的引用对象的作用是让你持有可被来及回收器自由回收的对象的引用,,换一种说法就是垃圾回收器可以修改任何非strongly引用的任何对象的状态。因为当你持有soft,weak,phantom引用的的很多时候垃圾回收器跟踪对象的可达性状态变换是很重要的,为了跟踪对象的可达性状态,你可以将引用对象和引用队列关联起来。引用队列是一个java.lang.ref.ReferenceQueue实例,在当对象的状态改变时来及回收器会将对应的对象入队,通过设置监控队列,当对象的可达性状态变化的时候你就可以收到垃圾回收器异步发送的通知。

要将一个引用对象和引用队列关联起来,你只需要将一个对象的引用传递个引用队列的构造方法,这样一个引用对象会被创建,它会持有一个指向引用者的引用,并且持有一个指向引用队列的引用。当垃圾回收器更爱一个相关的引用者的可达性状态的时候它就会将引用对象加到与对象关联的引用队列中,例如下图:当一个WeakReference对象被创建,两个引用:一个指向Fox对象的引用和一个指向ReferenceQueue对象的引用会被被传递给构造方法。当垃圾回收器决定回收那个weakly可达的Fox对象的时候,它会将WeakReference对象添加到队里中的同时活着稍后清除Weakreference对象。


垃圾回收器调用Reference超类的enqueue()方法将引用对象添加到与其关联的队列的末尾,enqueue()方法只有在对象满足以下条件时才将对象入队:引用对象是与队列想关联的;第一次在这个对象上调用enqueue方法。程序可以通过两种方法监控对象的引用,1:调用poll()方法进行轮询,2:调用remove方法进行阻塞。如果一个引用对象在队列调用poll活着remove方法的时候再队列中是处于等待状态,方法将会把对象从队列中移除并返回移除的对象。如果队列中没有对象,poll方法会直接返回null,而remove方法将会阻塞直到下一个引用对象入队。一旦有一个对象进入队列,remove方法就会将它移除并返回它。

垃圾回收器在不同的情况下入队soft,weak和phantom对象来表示三种不同的对象可达性状态变换。以下是对在六种不同的可达性状态和场景下状态变换的详细说明:

1:strongly可达:一个对象可以从根引用集合开始被任何对象跟踪到。一个对象的生命周期是从strongly可达开始的,只要从根引用集合可以跟踪到这个对象或者对象被别的strongly引用的对象引用,那么这个对象就会保持strongly引用状态。垃圾回收器也不会回收被strongly引用对象占用的空间。

2:softly可达:如果一个对象不是strongly可达的,但是可以通过根引用集合的一个或者多个soft引用对象找到它,那么这个对象就是softly可达的。垃圾回收器可能会回收这样的对象占用的空间,如果垃圾回收器将这个对象占用的空间回收,那么所有指向这个对象的soft引用都会被清除。当垃圾回收器清除一个与引用队列关联的soft引用对象的时候,垃圾回收器会将它入队。

3:weakly可达:一个既不是strongly可达也不是softly可达的对象,如果可以通过根引用集合的一个或者多个(没有被清除的)weak引用对象找到它,那么它就是weakly可达的。垃圾回收器必须回收被这种weakly引用的对象占用的内存空间。回收的时候,垃圾回收器会清除所有指向这个weakly引用对象的引用。当垃圾回收器清除某个与引用队列关联的weak引用对象的时候,它会将这个对象入队。

4:可复活的:一个对象不是strongly,softly,weakly可达,但是仍可能因为某些对象的finalize方法的执行变回以上三种状态,那么这个对象就是可复活的。

5:phantom可达:一个对象不是strongly,softly,weakly可达,经检测它也不可能被任何finalize方法复活,并且通过根引用集合的一个或者多个(未被清除的)phantom(虚位)引用对象找到,那么这个对象就是phantom可达的。一旦一个被虚位引用对象引用的对象变成了phantom可达的,垃圾回收器就会将它入队,垃圾回收器永远不会清除phantom引用。所有的虚位引用都必须被程序显示的清除。

6:不可达:不处于以上5种状态的对象就是不可达状态的对象,这样的对象占用的内存空间可以被垃圾回收器回收。


注意垃圾回收器在softly引用对象,weakly引用对象的引用者离开相关的可达性状态的时候才将它们入队,而对于phantom引用来说,是在引用者进入相关的状态时垃圾回收器才会将它们入队。你也可看出垃圾回收器在清除soft和weak引用对象是是在它们入队之前,而清除phantom引用对象是则不是这样处理的。因此,垃圾回收器在将soft引用对象入队时表示soft引用对象的引用者们刚刚离开softly可达状态,同样垃圾回收器入队weakly引用对象时表示它们的引用者们刚刚离开weakly可达状态。但是垃圾回收器入队phantom引用对象则是表示它们的引用这门刚刚进入phantom可达状态。phantom引用对象会一直保持这种状态直到它们的引用对象显示的被程序删除。


缓存,Canonicalizing映射和Pre-Mortem清理

垃圾回收器区别对待soft,weak和phantom对象的原因是这三种对象是为程序提供不同的服务而生的。soft引用使你能够创建内存内的缓存,这种缓存对整个程序所需要的内存空间的大小非常敏感。weak引用使你能够创建canonicalizing映射,比如哈希表,这种哈希表的键和值在程序不再引用他们的时候会被删除。phantom引用是你能够建立比finalize方法更灵活的pre-mortem清理策略。

如果要使用soft或者weak引用对象的引用者,你就需要在引用对象上调用get()方法,如果引用尚未被清理,那么get()方法会使你得到一个指向引用者的强引用(strong reference),这个时候你就可以以某种特殊的方式使用它。如果对象已经被清理掉了,get()返回null。如果你在一个phantom引用对象上调用get方法,它会一直返回null,即使这个引用对象还没有被垃圾回收器清理掉。因为phantom可达状态是在一个对象已经不可能被复活后才会出现的状态。phantom一样对象无法提供访问它的引用者的方法。因此如果一个对象到达phantom引用状态,那么它一定是无法被复活的。


虚拟机的实现要求在抛出OutOfMemoryError异常之前对soft引用进行清理,如果不会抛出此异常虚拟机何时清理soft引用使不确定的。然而,我们鼓励的虚拟机实现方式是:(1)在程序需要的内存空间的大小超过了虚拟机提供的内存空间的大小的时候才去清理soft引用;(2)在清理新的soft引用之前清理较老的soft引用;(3)先清理长时间未被使用过的soft引用,然后清理近期被使用过的soft引用。

soft引用使你能够将那些可以以较慢的速度从外部数据源获得的数据存储在内存缓存中,这样的外部包括文件,数据库和网络等。只要虚拟机拥有足够的内存存储堆栈上的所有strong引用和soft引用,soft引用通常就可以“足够强的”持有被soft引用引用的堆栈数据。然而如果内存变得很稀有,垃圾回收期就可能会决定去清理soft引用,回收它们占用的空间。下一次程序要是用那些数据的时候,就需要从外部的数据源去重新加载。


weak引用和soft引用很类似,有一点不同的是:垃圾回收器可以自由的决定何时释放soft引用,但是必须在检测到weak引用时尽快将weak引用释放掉。weak一样让你能够创建canonicalizing映射。java.util.WeakHashMap类使用weak引用提供canonicalizing映射。你可通过put方法向WeakHashMap实例中放入键值对,这和其他实现了java.util.Map的类的操作一样。但是在WeakHashMap内部,key对象是通过一个与引用队列关联的weak引用存储的,如果垃圾回收器检测到一个key对象是weakly可达状态,它就会清理任何引用key对象的弱引用对象并将它们入队。下一次访问WeakHashMap的时候,它就会轮询队列并提取出之前垃圾回收器放入队的对象。WeakHashMap然后就会从它的映射关系中移除任何key对象在队里中出现过的键值对。因此如果你添加一个键值对到WeakHashMap中,它就会一直存在于WeakHashMap中,直到程序显示的使用remove方法将它移除并且垃圾回收器也没有认为key对象是一个处于weakly可达状态的对象。


phantom可达表示一个对象已经准备好被回收了。当垃圾回收器认为一个phantom引用的引用者对象是phantom可达的,它就将这个phantom引用对象添加到一个关联的队列中(和soft,weak可以选择性的创建关联队列对象不同,phantom引用创建时必须有一个关联的队列)你可以使用phantom对象到达这个事件去触发一些你希望在对象生命终结时执行的操作。因为你无法获得一个指向phantom对象的strong引用(调用它的get方法只会返回null),你就无法采取那些要求你访问目标实例变量的操作。一旦你的phantom引用的pre-mortem清理操作已经完成,你就必须在phantom引用对象上调用clear方法,这将使phantom引用对象的引用者从phantom可达状态转换为不可达状态。


关于三种弱引用大家可以参考:http://blog.csdn.net/kx_nullpointer/article/details/8291936

未完,待续。。。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值