垃圾收集器与内存分配策略

垃圾回收三件事:哪些内存需要回收,什么时候回收,如何回收。

1. 判断对象是否已不可能被任何途径使用

引用计数法
  • 对象添加引用计数器,有引用加一,引用失效减一,计数器为0时对象不可能再被使用。
  • 问题 :很难解决循环引用的问题
可达性分析算法(主流使用)
  • 选择一些对象作为起始点(GC Roots),从这些节点向下搜索,如果有对象无法通过引用链和起始点相连,则证明该对象不可用
  • GC Roots对象类型:
    • 虚拟机栈中的对象
    • 方法区中静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中引用的对象
引用分类

2. 垃圾收集算法

标记 - 清除算法
  • 思路:标记需要回收的对象,标记完成后统一回收
  • 不足:
    • 标记和清除效率都不高
    • 会产生大量的内存碎片
复制算法
  • 思路:可用内存分为大小相等的两块,每次只使用其中一块,该块内存用完了,将存活的对象赋值到另外一块上,该块全部清除
  • 不足:浪费了一半的内存
  • 常用来回收新生代,因为新生代对象大部分都生命周期短,并不需要按照1:1的比例来划分空间。
  • (参考HotSpot默认Eden:Survivor = 8:1)
标记 - 整理算法
  • 标记需要被清理的对象,然后让存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
  • 常用来回收老年代
分代收集算法
  • 根据对象存活周期不同把内存划分为新生代和老年代,分别使用不同收集算法

3. HotSpot的算法实现

在基础的存活判定和垃圾收集算法基础上,HotSpot在实现时考虑了多种提高执行效率的方法。

  • 使用准确式GC,快速得知哪些位置是引用,以此来快速枚举根节点(GC Roots),减少GC停顿(暂停所有线程进行GC操作)的时间
  • 设立安全点(程序长时间执行时),在线程达到安全点的时候才暂停。使用主动式中断让线程轮询某个标记位,标记位为真时自己中断挂起
  • 设立安全区域,对于没有分配CPU时间的线程(Sleep或Blocked)标记为安全区域,该线程要离开安全区域时需要先查看根节点是否枚举完或是GC已完成才可以离开。

4. 垃圾收集器

  • HotSpot虚拟机包含的垃圾收集器
Serial 收集器

  • 单线程进行垃圾收集,垃圾收集时必须暂停其他所有工作线程。
ParNew 收集器

  • Serial收集器的多线程版本
Parallel Scavenge 收集器

  • 使用复制算法 + 并行的多线程收集器
  • 其他收集器的目的是尽可能缩短GC停顿时间,而Parallel Scavenge的目的是达到一个可控制的吞吐量。缩短GC停顿侧重的是加快响应速度,而吞吐量则是为了高效率利用CPU时间,尽快完成任务。这主要适合在后台运算而不需要太多交互的任务。
  • 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
Serial Old 收集器
  • Serial 收集器的老年代版本,单线程,使用“标记-整理”算法。
Parallel Old 收集器
  • Parallel Scavenge 收集器的老年代版本,多线程,使用“标记-整理”算法
  • 在注重吞吐量以及CPU资源敏感的场合,可以考虑使用Parallel Scavenge + Parallel Old的组合
CMS(Concurrent Mark Sweep) 收集器

  • 以获取最短回收停顿时间为目标,基于“标记-清除”算法实现
  • 步骤:
    • 初始标记(initial mark):GC停顿,标记下GC Roots能直接关联到的对象
    • 并发标记(concurrent mark):进行GC Roots Tracing,不GC停顿
    • 重新标记(remark):GC停顿,修正并发标记期间因程序运作导致的标记变动
    • 并发清除(concurrent sweep)无GC停顿,和用户线程一起工作
  • 优点:并发收集、低停顿
  • 缺点:
    • 会占用CPU资源导致应用变慢,吞吐量降低。
    • 无法处理浮动垃圾(Floating Garbage,并发清理时用户线程生成的垃圾,需要留到下一次GC时再清理),可能出现Concurrent Mode Failure导致Full GC。
    • “标记-清除”算法导致空间碎片过多
G1(Garbage-First) 收集器

  • G1收集器将堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔离,都是一部分Region的集合。
  • G1 通过跟踪各个Region里面的垃圾堆积的价值大小,维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
  • G1实现的时候region之间对象的互相引用,可以通过Remembered Set数据结构记录相关引用信息来避免全堆扫描。
  • G1 运作步骤:
    • 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,需要GC停顿。
    • 并发标记(Concurrent Marking):从GC Root开始对堆内对象进行可达性分析
    • 最终标记(Final Marking):修正并发标记期间因为用户线程运行导致标记产生变动的记录,将变化记录在Remembered Set Logs中,并合并到Remembered Set中,需要GC停顿。
    • 筛选回收(Live Data Counting and Evacuation):对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
  • 特点:
    • 并行与并发:并行减少GC停顿,并发让Java程序继续运行
    • 分代收集
    • 空间整合:从整体看基于“标记-整理”算法实现,从局部(两个Region之间)看是基于“复制”算法实现的。该两种算法都不会产生内存空间碎片,防止因为非配大对象无法找到连续内存空间而提前触发GC。
    • 可预测的停顿:能建立可预测的停顿时间模型,让使用者制定长度为M毫秒的时间片段内垃圾收集时间不能超过N毫秒。通过避免在整个堆中进行垃圾收集来实现。

5. 内存分配与回收策略

如何给对象分配内存以及回收分配给对象的内存,有如下几条普遍的内存分配规则:

对象优先在Eden分配

大多情况下,对象在新生代Eden区中分配,当Eden去没有足够空间时,发起一次Minor GC。

大对象直接进入老年代

大于 -XX:PretenureSizeThreshold 设置值的对象直接在老年代中分配,避免Eden区以及两个Survivor之间发生大量的内存复制。

长期存活的对象进入老年代

每个对象都有一个年龄计数器,对象在Eden出生后,每经历一次Minor GC,年龄加一,当年龄增加到一定程度会被移入到老年代。

动态对象年龄判断

如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,则大于等于该年龄的对象直接进入老年代,不用等年龄限制。

空间分配担保

因为复制算法中,老年代要作为新生代的分配担保。当Minor GC后存活对象容量大于Survivor空间时,就需要把多余的对象移入到老年代,所以老年代最大可用的连续空间要大于新生代所有对象总空间,这样才能保证Minor GC是确保安全的。可以通过设置 HandlePromotionFailure 来允许担保失败,当担保失败发生时,会触发一次Full GC。