深入虚拟机笔记之垃圾收集

第9章 垃圾收集

    在java虚拟机的堆里存放着正在运行的java程序所创建的所有对象。使用new、newarray、anewarray和multianewarray指令来创建对象,但没有明确的指令来释放它们。垃圾收集就是自动释放不再被程序所使用的对象的过程。

    当一个对象不再被程序所引用时,它所使用的堆空间可以被回收,以便被手续的新对象使用。垃圾收集器必须能断定哪些对象是不再被引用的,并且能够把它们所占用的堆空间释放出来。在释放的过程中,垃圾收集器运行将要被释放的对象的终结方法(finalizer)。

    垃圾检测通常通过建立一个根对象的集合,并且检查从这些根对象开始的可触及性来实现。如果正在执行的程序可以访问到的根对象与某个对象之间存在引用路径,那么这个对象就是可触及的。对于程序来说,根对象总是可以访问的,从这些根对象开始,任何无法被触及的对象被认为是垃圾,它们不再影响程序的未来运行。

    区分活动对象和垃圾的两个基本方法时引用计数和跟踪。引用计数垃圾收集器通过为堆中的每一个对象保持一个计数来区分活动对象和垃圾对象,引用计数记录下了对那个对象的引用次数。跟踪垃圾收集器实际上追踪从根节点开始的引用图,在追踪中遇到的对象以某种方式打上标记,当追踪结束时,没有被打上标记的对象被判定为不可触及的,可以被当作垃圾收集。

    引用计数垃圾收集器:引用计数是垃圾收集的早期策略。当一个对象被创建并且指向该对象的引用被分配给一个变量,这个对象的引用计数被置为1;当任何其他变量被赋值为堆这个对象的引用时,计数加1;当一个对象的引用超过了生存期或被设置一个新的值时,对象的引用计数减1。任何引用计数为0的对象都可以被当作垃圾收集。这种算法的缺点是:引用计数无法检测出循环引用(多个对象互相引用);每次引用计数的增加或者减少都带来额外的开销。

    跟踪垃圾收集器:因为引用计数方法固有的缺陷,这种技术已经不为人所接受。现实所遇到的java虚拟机更可能在垃圾收集中使用追踪算法。跟踪收集器追踪从根节点开始的对象引用图。基本的追踪算法被称作“标记并清除”,在标记阶段,垃圾收集器遍历引用数,标记每一个遇到的对象;在清除阶段,未被标记的对象被释放了,使用的内存被返回给正在执行的程序;清除步骤必须包括对象的终结。标记并清除收集器通常使用两种策略对付堆碎块:压缩和拷贝。

    压缩收集器:把活动的对象越过空闲区滑动到堆的一端。在这个过程中,堆的另一端出现一个大的连续空闲区,所有被移动的引用也被更新,指向新的位置。更新被移动的对象引用有时候通过一个间接对象引用层。不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表;对象句柄才指向堆中对象的实际位置。当对象被移动了,只有这个句柄需要被更新为新位置。这种方法简化了消除碎块的工作,但每一次对象访问都带来性能损失。

    拷贝收集器:把所有活动对象移动到一个新的区域。在拷贝的过程中,它们被紧挨着布置,所以可以消除原本他们在旧区域的空隙。原本的区域被认为都是空闲区。这种方法的好处是对象可以在从根对象开始遍历的过程中随着发现而被拷贝,不再有标记和清除的区分。对象被快速拷贝到新区域,同时转向指针仍然留在原来的位置。转向指针可以让垃圾收集器发现已经被转移的对象的引用,然后垃圾收集器可以把这些引用设置为转向指针的值,所以它们现在指向对象的新位置。拷贝收集器算法被称为“停止并拷贝”。这个方案中,堆被分为两个区域,任何时候都只使用其中的一个区域;对象在同一个区域中分配,直到这个区域被耗尽;此时,程序执行被中止,堆被遍历,遍历时遇到的活动对象被拷贝到另一个区域;当停止和拷贝过程结束时,程序恢复执行,内存将从新的堆区域中分配,直到它也被耗尽。这种方法的代价是,对于指定大小的堆来说需要两倍大小的内存。

 

    按代收集的收集器:简单的停止拷贝收集器的缺点是,每一次收集时,所有的活动对象都必须被拷贝,它每次都把那些生命周期很长的对象来回拷贝,消耗大量的时间。按代收集的收集器通过把对象按照寿命来分组解决这个效率低下的问题,更多地收集那些短暂出现的年幼对象,而非寿命较长的对象。在这种方案中,堆被分成多个子堆,每个子堆为一代对象服务;最年幼的那一代进行最频繁的垃圾收集,如果一个最年幼的对象进过几次垃圾收集后仍然存活,那么它就被转移到另外一个代表更高寿命的子堆中去;每当对象在它所属的子堆中变得成熟(逃过多次垃圾收集)之后,它们就被转移到代表更高年龄的子堆中去。

    自适应收集器:在某种情况下某些垃圾收集算法工作的更好,而另外一些收集算法在另外的情况下工作的更好。自适应算法监视堆中的情形,并且对应地调整为合适的垃圾收集技术。

    火车算法:垃圾收集算法和明确释放对象比起来有一个潜在的缺点,即垃圾收集算法中程序员堆安排CPU时间进行内存回收缺乏控制。因为垃圾收集一般都会停止整个程序的运行来查找和收集垃圾对象,垃圾收集可能使得程序对事件响应迟钝,无法满足实时系统的要求。达到非破坏性垃圾收集的方法是使用渐进式的收集算法。渐进式垃圾收集器不会试图一次性发现并回收所有不可触及的对象,而是每次发现并回收一部分,因此理论上说每一次收集会持续更短的时间,如果每次可以保证(或者非常接近)不超过一个最大时间长度,就可以让java虚拟机适合实时环境;这样的收集器也可以消除用户可察觉的到的垃圾收集停顿。

    通常渐进式垃圾收集器都是按代收集的收集器,大部分调用中都是收集堆的一部分。大部分对象都很快消亡,按代收集的收集器在年幼的子堆中比在年长的子堆中活动更频繁;除了最高寿的那个子堆(成熟对象空间)之外,每一个子堆都可以给定一个最大尺寸,按代收集的收集器可以大体上保证在一个最大时间值内渐进地收集所有的对象。成熟对象空间无法给定最大尺寸,因为,任何在其他年龄层子堆中不再适合的对象总要有个去处,除了成熟对象空间,它们没处可去。火车算法的目的是为了在成熟对象空间提供限定时间的渐进收集。

     火车算法把成熟对象空间划分为固定长度的内存块,算法每次只会在一个块中单独执行。每一个块归属于一个集合,在一个集合内的块排了序,这些集合本身也排了序。火车算法中,块被称为车厢,集合被称为火车,成熟空间扮演火车站的角色。火车按照他们创建时的顺序分配号码,号码较小的火车总是更早出现的火车。在火车内部,车厢(块)总是被附加到火车的尾部,因此,较小数字表示更早出现的车厢。用这种命名方式表示成熟对象空间中所有的块的总体顺序。

    火车算法每次执行的时候,只会对一个块(号码最低的块)执行垃圾收集。对象从更年轻的子堆提出来进入成熟对象空间,不管何时提出,它们都被附加到任何已经存在的火车中(最小号码火车除外),或者专门为容纳它们创建的一列新火车中。

    车厢收集:每次火车算法被执行的时候,它要么收集最小数字火车中的最小数字车厢,要么收集整列最小数字火车。算法首先检查执行整列火车中任何车厢的引用,如果不存在任何来自火车以外的引用指向它内部包含的对象,那么整列火车都是垃圾,算法归还火车中所有车厢中的对象并返回。这步算法使得火车算法可以一次收集大型的,无法在一个块中容纳的循环数据结构。如果火车里并不都是垃圾,那么算法把注意力放到最小数字车厢上。算法首先把被车厢外部的车厢引用的对象转移到其他车厢去;当进行这个移动后,车厢里任何保留下来的对象都是没有引用的,可以被垃圾收集。算法归还最小数字车厢占据的空间并返回。

    保证整列火车中没有循环的数据结构的关键是算法如何移动对象。如果正被收集的车厢中有一个对象被来自成熟空间以外的对象引用,这个对象被转移到正在被收集的火车之外的其他车厢去。如果对象被成熟对象空间的其他火车引用,对象就被转移到引用它的那列火车中去;然后转移过去的对象被扫描,查找对原车厢的引用,发现的任何被转移对象引用的对象都被转移到引用它的火车中去;新转移的对象也被扫描,这个过程不断重复,直到没有任何来自其他火车的引用指向正被收集的那节车厢。如果接收对象的车厢没有空间了,算法会创建新的车厢并附加到那列火车的尾部。一旦没有来自火车外的引用了,那么这节车厢剩余的外部引用都是来自于同一列火车的其他车厢;算法把这样的对象转移到最小数字火车的最后一个车厢去,然后扫描这些对象,查找对原被收集车厢的引用,任何新发现的被引用对象也都被转移到同一列列车的尾部,这个过程不断重复,直到没有任何形式的引用指向被收集的车厢。然后算法归还整个最小数字车厢占据的空间并且返回。

    火车算法最重要的方面之一,就是它保证大型的循环数据会完全被收集,即使它们不能被放置在一个车厢中。因为对象被转移到引用它们的火车,相关的对象会变得集中。最后,成为垃圾的循环数据结构中的所有对象,不管有多大,会被放置到同一列火车中去,增大循环数据结构的大小只会增加最终组成同一列火车的车厢数。

    记忆集合:为了促进收集过程,火车算法使用了记忆集合。一个记忆集合是一个数据结构,它包含了所有对一节车厢或者一列火车的外部引用。算法为成熟对象空间内每节车厢和每列火车都维护一个记忆集合。一个空的记忆集合显示车厢或者火车中的对象都不再被外部引用,它们是不可触及的,可以被垃圾收集。

猜你喜欢

转载自jaesonchen.iteye.com/blog/2289408