Java GC

2018-04-20 20:20:24
共1.9k字 预计阅读6分钟

GC(Garbage Collection) 垃圾收集 目标主要为堆内存

四种基本回收算法 流行的垃圾收集算法一般是由四种中的其中几种组合而成

  1. 引用计数算法(reference counting)

  2. 标记-清除算法(mark-sweep)

  3. 标记-压缩算法(mark-compact)

  4. 复制算法(copying)

  5. 引用计数法
    每个对象在创建时 会绑定一个计数器 每当有一个引用指向该对象时 计数器加一
    每当有一个指向它的引用被删除时 计数器减一 计数器为零时 该对象死亡

无法消除循环引用垃圾对象 循环引用对象的引用计数器不会为0

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = new Object();
o1.ref = o2;
o2.ref = o1;
o1 = null;
o2 = null;
}
  1. 标记-清除法
  • 分为标记和清除两个阶段
    • 标记阶段 从GC Roots向下遍历所有根结点 遍历路径称为引用链
      一个对象到GC Roots无任何引用链相连时 则此对象为不可达对象
    • 清除阶段 遍历堆中所有对象 清除没有被标记的对象
  • 速度快 会造成内存碎片
  1. 标记-压缩算法
  • 对标记-清除算法的一种改良 分为标记和压缩两个阶段
    • 标记阶段 与标记-清除法标记阶段一致
    • 压缩阶段 将所有标记对象移动到另一处内存 然后清除剩余对象
  • 速度慢 不会造成内存碎片
  1. 复制算法
  • 将内存平均分成两部分 每次只使用其中一部分
    部分1内存满时 将内存中所有存活对象复制到部分2内存
    然后将部分1内存清空
  • 速度快 不会造成内存碎片 会浪费内存

常用回收算法

  • 分代收集
    按照对象对象存活周期的不同 将内存划分为几块 一般把Java堆分为新生代和年老代
    根据各个年代采用最合适算法
    新生代 每次垃圾回收只有少量存活 使用复制法
    年老代 对象存活较久 没有额外的内存担保空间 使用复制法标记清除或标记压缩

10种垃圾回收器

garbage-collectors

  • Serial收集器 新生代使用
    暂停所有工作线程 采用复制算法
    最基本 发展历史最悠久的收集器
    JDK1.3.1前是HotSpot新生代收集唯一选择 一直到到JDK1.7 依然是JVM client模式下默认新生代收集器
    单个cpu环境 Serial收集器由于没有线程交互开销 可获得最高的单线程收集效率
    在桌面应用中 分配给虚拟机管理的内存一般不会很大 收集几十兆甚至一两百兆的新生代
    停顿时间完全可以控制在几十毫秒最多一百多毫秒以内
    但随着内存越来越大 执行效率会越来越慢 如 一个人打扫5平米房间很快 打扫500平米房间很慢
    后面出现了多线程版本 ParNew收集器

  • Serial Old收集器 老年代使用
    与Serial相同 但 采用标记压缩算法

  • Parallel Scavenge 新生代使用
    Serial的多线程版本
    采用复制算法 与前两种收集器最大区别为 关注的是吞吐量而不是延迟
    吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
    适合在后台运算而不是太多交互的任务 高吞吐量可最高效率的利用cpu时间
    尽快的完成程序的运算任务 要降低停顿时间 也会影响吞吐量

  • Parallel Old 老年代使用
    Serial Old的多线程版本 标记-压缩算法

  • ParNew收集器 新生代使用
    等用于Parallel Scavenge 配合CMS使用

  • CMS(Concurrent Mark Sweep)

  • 一款真正的并发收集器 在线程执行过程中也可进行垃圾收集
    分为初始标记 并发标记 重新标记和并发清理四个阶段

  • 指定使用CMS后 会默认使用ParNew作为新生代收集器

  • 碎片化非常严重时 使用Serial Old清除

下面为不分代收集器

  • G1(Garbage First)
    JDK7中加入 HotSpot重点发展的垃圾回收技术 同优秀的CMS垃圾回收器一样
    G1关注响应时间垃圾回收器 也同样适合大尺寸堆内存的垃圾收集 官方推荐使用G1来代替选择CMS
    G1最大的特点是引入分区的思路 弱化了分代概念 合理利用垃圾收集各个周期的资源
    G1收集器 比前面更优秀 真正有突破的一款垃圾收集器 在G1中保留了分代的概念 但实际上新生代和老年代中没有物理隔离
    在G1中 内存空间被分割成一个个Region区 新生代和老年代 都由一个个region组成 同时G1不需要跟其它收集器一起配合使用
    自己就可搞定所有内存区域 即不是一个分代收集器 是一个通吃收集器 也是JVM内存管理和垃圾收集发展趋势

G1采用了标记-压缩算法 避免CMS内存碎片问题 另外能达到可控的垃圾时间

G1工作过程

初始标记 标记GC Root关联的对象

并发标记 分析GC Root到所有对象的可达性分析 从GC Root节点开始遍历所有对象会比较耗时 JVM是使用Remembered Set保存对象引用的调用信息
在可达性分析的时候只需要同时遍历remembered set 不需要从根节点开始挨个遍历

最终标记 由于并发标记阶段 用户线程仍然在工作 会对标记产生一些偏差 这时候需要通过remembered set log来记录这些改变
在此阶段将改变合并到remembered set中

筛选清除 通过标记压缩算法 根据用户配置的回收时间 和维护的优先级列表 优先收集价值最大的region 收集阶段是基于标记-压缩和复制算法实现

  • ZGC
    JDK11中新发布收集器 无分代概念 官方给出的是无碎片 时间可控 超大堆

  • Shenandoah
    Shenandoah是一款concurrent及parallel的垃圾收集器 跟ZGC一样也是面向low-pause-time的垃圾收集器
    不过ZGC是基于colored pointers来实现 而Shenandoah GC是基于brooks pointers来实现
    其实低停顿的GC 业界早就出现 只不过Java比较晚
    Azul的Zing中C4 GC 土豪选择
    oracle中的HotSpot ZGC JDK11的选择
    R大说ZGC说抄袭Azul的 两者是等价的
    可以参考 这篇文章学习连接

  • Epsilon
    Java 11 新的Epsilon垃圾收集器
    Epsilon(A No-Op Garbage Collector) 垃圾回收器控制内存分配
    但不执行任何垃圾回收工作 一旦Java的堆被耗尽 JVM就直接关闭
    设计的目的是提供一个完全消极的GC实现 分配有限的内存分配
    最大限度降低消费内存占用量和内存吞吐时的延迟时间 一个好的实现是隔离代码变化
    不影响其他GC 最小限度的改变其他的JVM代码

JVM默认垃圾收集器

  • JDK1.7 1.8 默认垃圾收集器Parallel Scavenge(新生代) + Parallel Old(老年代)
  • JDK1.9 默认垃圾收集器G1