分代回收机制及垃圾回收算法
GC 分类
- 新生代回收(Minor GC/Young GC):指只是进行新生代的回收。
- 老年代回收(Major GC/OldGC):指只是进行老年代的回收。目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。 (Major GC定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)
- 整堆回收(FullGC):收集整个 Java 堆和方法区(注意包含方法区)
垃圾回收算法
- 复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使 用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整;空间利用率只有一半
Appel 式回收
具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间,提高空间利用率和空间分配担保,当 Survivor 空间不够用时,需要 依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
- 标记-清除算法(Mark-Sweep)
算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。
问题:回收效率略低;标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连 续内存而不得不提前触发另一次垃圾回收动作
- 标记-整理算法(Mark-Compact)
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端 边界以外的内存。
问题:效率偏低;没有内存碎片;标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用 对象的地方都需要更新(直接指针需要调整)
常见的垃圾回收器
在新生代中,每次垃圾回收时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成回收。 而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
回收器 | 回收对象和算法 | 回收器类型 |
---|---|---|
Serial | 新生代,复制算法 | 单线程(串行) |
Serial Old | 老年代,标记整理算法 | 单线程(串行) |
Parallel Scavenge | 新生代,复制算法 | 并行的多线程收集器 |
Parallel Old | 老年代,标记整理算法 | 并行的多线程回收器 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 |
CMS | 老年代,标记清除算法 | 并发的多线程回收器 |
G1 | 跨新生代和老年代;标记整理 + 化整为零 | 并发的多线程回收器 |
- 单线程垃圾回收器
只适合几十兆到一两百兆的堆空间进行垃圾回收(可以控制停顿时间再 100ms 左右),但是对于超过这个大小的内存回收速度很慢 (-XX:+UseSerialGC参数设置)
- 多线程并行垃圾回收器
适合回收堆空间上百兆~几个 G
- 并发垃圾垃圾回收器
ParNew
多线程垃圾回收器,与 CMS 进行配合,对于 CMS(CMS 只回收老年代),新生代垃圾回收器只有 Serial 与 ParNew 可以选。和 Serial 基本没区别,唯一的区 别:多线程,多 CPU 的,停顿时间比 Serial 少
Concurrent Mark Sweep (CMS)
- 初始标记-短暂,仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
- 并发标记-和用户的应用程序同时进行,进行 GCRoots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)
- 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除
问题:
- CPU 敏感:CMS 对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足 4 个时,CMS 对用户的影响较大。
- 浮动垃圾:由于CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。 在 1.6的版本中老年代空间使用率阈值(92%) 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片
当碎片较多时,给大对象的分配带来很大的麻烦,为了解决这个问题,CMS 提供一个 参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。 这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡
Garbage First(G1)
设计思想
G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是 新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果
Region
Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过 了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次 幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous Region 作为老年代的一部分来进行看待
- 初始标记( Initial Marking) 仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。 这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
TAMS 是什么? 要达到 GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针, 从 Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。 - 并发标记( ConcurrentMarking) 从 GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫 描 完 成以 后 , 并 发 时 有 引 用 变 动 的 对 象 , 这 些 对 象 会 漏 标 ( 后 续 再 讲 三 色 标 记 的 时 候会 细 讲 这 个 问 题 ) , 漏 标 的 对 象 会 被 一 个 叫 做SATB(snapshot-at-the-beginning)算法来解决(这个下节课会细讲)
- 最终标记( Final Marking)对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的 SATB 记录(漏标对象)。
- 筛选回收( Live DataCounting and Evacuation) 负责更新 Region 的统计数据,对各个 Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动, 是必须暂停用户线程,由多条收集器线程并行完成的