术语
-
垃圾(Carbage):需要回收的对象。
-
根(Root):判断对象是否可被引用的起始点。
GC的三种基本方式
标记清除(Mark and Sweep)- 处理垃圾少的情况
最早开发出的GC算法。其原理是首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。
标记清除算法的处理时间,是和存活对象与对象总数的总和相关的。
作为标记处理清除的变形,还有一种叫做标记压缩(Mark and Compact)的算法,它不是将被标记的对象清除,而是将其不断压缩。
复制收集(Copy and Collection)- 处理垃圾多的情况
标记清除算法有一个缺点,就是在分配了大量对象,而且其中只有一小部分存活的情况下,所消耗的时间会大大超过必要的值,这是因为在清除阶段还需要对大量的死亡对象进行扫描。
复制收集则试图克服这一缺点。其会将从根节点开始被引用的对象复制到另外的空间中,然后,再将复制的对象所能够引用的对象用递归的方式不断复制下去。
和标记相比,将对象复制一份所需的开销则比较大,因此在”存活“对象比例较高的情况下,反而会比较不利。
这种算法的另一个好处是局部性。在复制收集的过程中,会按照对象被引用的顺序将对象复制到新空间中。于是,关系较近的对象被放在距离较近的内存空间中的可能性会提高,这被称为局部性。在局部性高的情况下,内存缓存会更容易高效运作,程序的运行性也能够提高。
引用计数(Reference Count)- 简单,但有致命缺陷
是GC算法中最简单也最容易实现的一种,其会在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新,当一个对象的引用计数变为0时,释放其空间。
优点是容易实现,及当对象不再被引用的瞬间就会被释放,没有其他GC算法的长时间中断。
缺点
-
无法释放循环引用的对象。
-
必须在引用发生增减时对引用计数做正确的增减。
-
不适合并行处理。
依然使用引用计数的语言主要有Perl和Python,但它们为了避免循环引用的问题,都配合使用了其他的GC机制。
进一步改良的应用方式
分代回收(Generational GC)- 减少扫描的数目
基本思路是利用了一般性程序所具备的性质,即大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。如果对分配不久,诞生时间较短的“年轻”对象进行重点扫描,应该就可以更有效地回收大部分垃圾。
在分代回收中,对象按照生成时间进行分代,刚刚生成不久的年轻对象划为新生代(Young generation),而存活了较长时间的对象划为老生代(Old generation)。
只扫描新生代对象的回收操作,被称为小回收(Minor GC)。小回收的具体回收步骤如下。
首先从根开始一次常规扫描,找到“存活”对象。这个步骤采用标记清除或者是复制收集算法都可以,不过大多数分代回收的实现都采用了复制收集算法。需要注意的是,在扫描的过程中,如果遇到属于老生代的对象,则不对该对象继续进行递归扫描。这样一来,需要扫描的对象数量就会大幅减少。然后,将第一次扫描后残留下来的对象划分到老生代。具体来说,如果是用复制收集算法的话,只要将复制目标空间设置为老生代就可以了;而用标记清除算法的话,则大多采用在对象上设置某种标志的方式。
虽说老生代区域中的对象一般来说寿命都比较长,但也决不是“不老不死”的。随着程序的运行,老生代区域中的“死亡”对象也在不断增加。为了避免这些死亡的老生代对象白白占用内存空间,偶尔需要对包括老生代区域在内的全部区域进行一次扫描回收。像这样以全部区域为对象的 GC 操作被称为完全回收(Full GC)或者大回收(Major GC)。
分代回收通过减少 GC 中扫描的对象数量,达到缩短 GC 带来的平均中断时间的效果。不过由于还是需要进行大回收,因此最大中断时间并没有得到什么改善。从吞吐量来看,在对象寿命假说能够成立的程序中,由于扫描对象数量的减少,可以达到非常不错的成绩。但是,其性能会被程序行为、分代数量、大回收触发条件等因素大幅度左右。
增量回收(Incremental GC)- 提高实时性
为了维持程序的实时性,不等到 GC 全部完成,而是将 GC 操作细分成多个部分逐一执行。
由于增量回收的过程是分步渐进式的,可以将中断时间控制在一定长度之内。另一方面, 由于中断操作需要消耗一定的时间,GC 所消耗的总时间就会相应增加,正所谓有得必有失。
并行回收(Parallel GC)
通过最大限度利用多 CPU 的处理能力来进行 GC 操作的一种方式
Java中的内存管理
参照
- 松本行弘《代码的未来》
网友评论