0%

【gc】常见的垃圾搜集算法

[TOC]

这里垃圾回收是指语言层面的对堆上的不再使用的对象,释放他们占用的空间。语言层面上的垃圾回收是一种自动内存管理(automatic memory management that consists of determining which objects should be deallocated (“garbage collected”))

通过这篇文章,你会知道:

  • 垃圾手机常见的判断对象是否可以回收的算法:引用计数法,跟可达分析法。

  • 垃圾回收的常见的几种算法:标记清理, 标记整理,标记复制,分代收集,三色标记。

判断对象是否可回收

引用计数算法(reference counting)

引用计数的思想非常简单:每个单元(对象)维护一个域(再对象头里),保存其它单元指向它的引用数量(类似有向图的入度)。如果多一个对这个对象的引用(指针),那么计数加一;而删除某个指向这个对象的引用(指针),那么计数减一。当其引用计数为 0 的时候,该单元会被进行回收。

  • 优点
    • 渐进式。内存管理与用户程序的执行交织在一起,将 GC 的代价分散到整个程序。
    • 内存单元能够很快被回收。相比于其他垃圾回收算法,堆被耗尽或者达到某个阈值才会进行垃圾回收。
    • 算法易于实现。
  • 缺点
    • 原始的引用计数不能处理循环引用。
    • 维护引用计数降低运行效率。这是引用计数的代价,其他的垃圾收集也会有对应的代价方式。

可达性分析算法

通过一系列称为 “GC Roots” 的对象作为起始点,从这个节点向下搜索,搜索走过的路径就是引用链,当一个对象到 GC Roots 没有任何引用链相连,也就是从 GC Roots 到这个对象不可达,则这个对象不可达,可以被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

简述Java方法区

方法区又被称为静态区,是程序中永远唯一的元素存储区域。和堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。很多开发者更愿意把方法区称为“永久代”(Perm Gen)(Permanent Generation) 「总是存放不会轻易改变的内容」。在目前已经发布的JDK 1.7 的HotSpot中,已经把原本放在永久代的字符串常量池移至堆中。运行时常量池(Runtime Constant Pool)是方法区的一部分。

常见垃圾收集算法

标记-清除(mark and sweep)

标记-清扫算法是一种基于追踪的自动内存管理。内存单元在程序运行过程中并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW(Stop The World,GC 的时候挂起用户程序),转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。算法分两个部分:标记(mark)和清扫(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。如下图:

过程

在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。

在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。

在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。

优点

  • 可以处理循环引用的问题。

缺点

  • 在垃圾回收阶段会停止运行程序,大大增加程序的响应时间和不稳定性。
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

标记-整理(Mark-Czompact)

为了提升内存的利用率,科学家提出了标记-整理算法,该算法的起始过程和标记-清除算法相同,先标记处待回收对象的内存区域,但是在清除时不是对所有可回收对象清除,而是让所有存活对象往内存空间的一边移动,把存活对象边界外的内存直接清空掉。

优点

  • 不会产生内存碎片

缺点

  • 需要移动大量对象,处理效率比较低。
  • 在移动存活对象的过程中,需要全程暂停用户程序的执行,被设计者称为“Stop The World”。

标记-复制算法(Mark-Copying)

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

优点

  • 比较快

缺点

  • 主要不足是只使用了内存的一半。

分代收集(generation collector)

分代收集算法本质上是标记-复制算法,它把堆内存中较大的一块区域作为新生代区域,新生代区域中分为一个Eden区域和两个Survivor区域,Eden和Survivor的比例默认是8:1,因为在Eden区域,绝大数对象都熬不过第一轮GC(98%),所以每个Survivor区域只需要10%的空间就足矣了,每一次触发Minor GC时,就会将Eden区和Survivor区存活的对象复制到另外一个Survivor区域中,然后清除掉被回收的对象,每次都依据这样的步骤进行垃圾收集。

优点

  • 效率较高

缺点

  • 实现复杂,且依赖于 标记-复制 垃圾收集算法

分配在old的情况

大对象。当对象所占连续内存非常大时,不会分配在Eden区,如果分配在Eden区,那么对象存活时产生的复制操作将导致效率大大降低。

Stop The World

需要先了解针对不同区域进行收集的名词:Minor GC(新生代收集)、Major GC(老年代收集)、Full GC(整个Java堆和方法区的收集)、Mixed GC(新生代收集和部分老年代的收集,目前只有G1收集器有这种行为)

发生GC(MinorGC或者FullGC)时,都会将用户线程停顿并进行垃圾收集,在Minor GC中,STW的时间较短,只涉及Eden和survivor区域的对象清除和复制操作,而Full GC则是对整个堆内存进行垃圾收集,对象的扫描、标记和清除操作工作量大大提高,所以Full GC会导致用户线程停顿较长时间,如果频繁地发生Full GC,那么用户线程将无法正常执行。

三色标记(Tri-color_marking)

三色标记法是一个逻辑上的抽象,将对象分成白:未搜索,灰:正搜索,黑:已搜索。

有三种集合:白色,黑色,灰色

  • 白色集合,或者叫危险集合,是垃圾回收的候选对象的集合
  • 黑色集合,黑色集合是根集合可达的集合,且没有白色集合中对象的传出引用,不参与垃圾回收
  • 灰色集包含从根可到达的所有对象,但尚未扫描以查找对“白色”对象的引用。 由于已知它们可以从根部到达,因此它们不能被垃圾收集,并且在扫描后将最终成为黑色集合。

三色标记详细的请看 golang垃圾回收

优点

  • stop the world 时间较短

缺点

  • 三色标记必须引入写屏障,这带来了一些额外的开销
  • 写屏障还会带来垃圾残留,因为一些可能被用到的对象在标记过程中会被标记为灰节点

参考

一张图了解三色标记

维基百科-三色标记

java垃圾收集算法