垃圾收集(Garbage Collection,简称GC),围绕三个方向:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
对象已死?
在堆里面存放着
Java
世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就 是要确定这些对象之中哪些还“
存活
”
着,哪些已经
“
死去
”
(“死去
”
即不可能再被任何途径使用的对象)了
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。客观地说,引用计数算法( Reference Counting )虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。也有一些比较著名的应用案例,例如微软COM ( Component Object Model )技术、使用 ActionScript 3 的 FlashPlayer 、 Python 语言以及在游戏脚本领域得到许多应用的Squirrel 中都使用了引用计数算法进行内存管理。但是,在 Java领域,至少主流的Java 虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析算法
当前主流的商用程序语言(Java、 C# ,上溯至前面提到的古老的 Lisp )的内存管理子系统,都是通过可达性分析(Reachability Analysis )算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“GC Roots” 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“ 引用链 ” ( Reference Chain ),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
如图
所示,对象
object 5
、
object 6
、
object 7
虽然互有关联,但是它们到
GC Roots
是不可达的, 因此它们将会被判定为可回收的对象
![](https://i-blog.csdnimg.cn/blog_migrate/fe3522dbacaffacbc25b3c8ce010f636.png)
即使在可达性分析算法中判定为不可达的对象,也不是 “ 非死不可 ” 的,这时候它们暂时还处于 “ 缓刑” 阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没 有与GC Roots 相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法。假如对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“ 没有必要执行 ” 。
回收方法区
方法区垃圾收集的“ 性价比 ” 通常也是比较低的:在 Java 堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常可以回收70% 至 99% 的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。回收废弃常量与回收Java堆中的对象非常类似。举个常量池中字面量回收的例子,假如一个字符串 “java” 曾经进入常量池中,但是当前系统又没有任何一个字符串对象的值是“java” ,换句话说,已经没有任何字符串对象引用常量池中的“java” 常量,且虚拟机中也没有其他地方引用这个字面量。如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java” 常量就将会被系统清理出常量池。常量池中其他类(接口)、方法、字段的符号引用也与此类似。
垃圾收集算法
分代收集理论
当前商业虚拟机的垃圾收集器,大多数都遵循了
“
分代收集
”
(
Generational Collection
)
[1]
的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:
1
)弱分代假说(
Weak Generational Hypothesis
):绝大多数对象都是朝生夕灭的。
2
)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将
Java
堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为:■ 新生代收集( Minor GC/Young GC ):指目标只是新生代的垃圾收集。■ 老年代收集( Major GC/Old GC ):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单 独收集老年代的行为。另外请注意“Major GC” 这个说法现在有点混淆,在不同资料上常有不同所指,按上下文区分到底是指老年代的收集还是整堆收集。■ 混合收集( Mixed GC ):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。· 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
标记-清除算法
最早出现也是最基础的垃圾收集算法是 “ 标记 - 清除 ” ( Mark-Sweep )算法,在 1960 年由 Lisp 之父John McCarthy所提出。如它的名字一样,算法分为 “ 标记 ” 和 “ 清除 ” 两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程,这在前一节讲述垃圾对象标记判定算法时其实已经介绍过了。之所以说它是最基础的收集算法,是因为后续的收集算法大多都是以标记 - 清除算法为基础,对其缺点进行改进而得到的。它的主要缺点有两个:第一个是执行效率不稳定,如果Java 堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
![](https://i-blog.csdnimg.cn/blog_migrate/d530c7687becdb8b540ed4f655a0dde9.png)
标记-复制算法
标记 - 复制算法常被简称为复制算法。为了解决标记 - 清除算法面对大量可回收对象时执行效率低的问题,1969 年 Fenichel 提出了一种称为 “ 半区复制 ” (Semispace Copying)的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。
![](https://i-blog.csdnimg.cn/blog_migrate/7a3d6c03379761147f8f1b79f27c4784.png)
标记-整理算法
标记 - 复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50% 的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100% 存活的极端情况,所以在老年代一般不能直接选用这种算法。针对老年代对象的存亡特征, 1974 年 Edward Lueders 提出了另外一种有针对性的 “ 标记 - 整理 ” ( Mark-Compact )算法,其中的标记过程仍然与 “ 标记 - 清除 ” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,“ 标记 - 整理 ” 算法的示意图如图 3-4 所示。标记 - 清除算法与标记 - 整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。
![](https://i-blog.csdnimg.cn/blog_migrate/7595e69ebab56c9f30a85bebca5a1373.png)