一. 概述
如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现. 垃圾收集器的选择,并非是找出最好的,而是最适合的. 直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器.
二. 垃圾收集算法
当前绝大多数(几乎所有)垃圾收集器垃圾回收算法都是基于分代收集理论实现的. 根据对象生存周期不同,采用不同回收算法.
三. 垃圾收集器
3.1 Serial 串行垃圾收集器
算法
- 年轻代: 复制算法
- 老年代: 标记整理
串行
- 单线程
- gc阶段STW(Stop The World)
优点
- 简单:单线程收集,实现简单
- 高效:相同cpu资源,收集最高效
缺点
- 全程STW
- 单线程: 多核处理器下无法充分利用服务器资源
核心参数
-XX:+UseSerialGC -XX:+UseSerialOldGC
3.2 Parallel 并行垃圾收集器
Serial的多线程版本. 通过提高单位时间内的垃圾收集吞吐量,来降低STW时间.
算法
- 年轻代: 复制算法
- 老年代: 标记整理
并行
- 多线程
优点
- 简单:相对简单
- 高效:多核处理器环境下,支持多线程,充分利用服务器资源提高. 通过提高单位时间吞吐量,减少STW
缺点
- 全程STW
** 开启**
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads: 指定收集线程数,默认的收集线程数跟cpu核数相同
3.3 ParNew
基本同Parallel .不同点在于它支持和CMS搭配使用.
-XX:+UseParNewGC
3.4 CMS 并发垃圾收集器
一款真正的并发垃圾收集器. 以获取最短STW为目标的收集器. 和上述收集器最大的不同是:将垃圾收集分为多个阶段,且在部分阶段支持gc线程和用户线程并行.
3.4.1 收集步骤(无营养版)
- 初始标记: STW. 记录GCRoots直接引用的对象. 很快.
- 并发标记: gc线程和用户线程同时运行. 从GC Roots的直接关联对象开始遍历整个对象图的过程.这个过程很耗时,但是并不STW.
- 重新标记(最终标记): STW. 耗时比初始标记大,但远比并发标记阶段耗时小.利用三色标记 通过增量更新算法修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清理: gc线程和用户线程同时运行. 清理垃圾. 这个阶段新生成的对象直接标记为(黑色)非垃圾.
- 并发重置: 清除标记
3.4.2 三色标记
利用三个颜色表示是否已完全扫描,是否可达
- 黑色 可达且直接引用已完全扫描. --- 不会再扫描了
- 灰色 可达但存在直接引用未扫描. --- 以它为root 接着扫描
- 白色 表示不可达或者未扫描.
3.4.3 收集步骤(营养版)
初始标记: gc root 全部为黑色. gc root 直接引用的对象可能为灰色或者黑色.
并发标记: 这个阶段,多线程标记,存在以下标记状态:
image.png
这个阶段gc线程和用户线程并发执行. 可能存在以下情况:
- set O3.o5=null ; O1.o5=O5;
- 局部变量R2随着线程执行结束而销毁
- 新的线程创建新的对象. R3.o7=new O7();
image.png
如果不此时直接根据标记结果回收对象,会存在多标和漏标
-多标 局部标量R2引用的对象O6已经是垃圾,但是被标记为黑色. 这些垃圾称为浮动垃圾,可以接受,下次gc回收. O7页可能是浮动垃圾.
-漏标 O3断开了和O5的引用关系,而O1已经是黑色,表示已经扫描完成. O5非垃圾对象,但是标记完成还是白色不可达. 这种情况是不可接受的,可能造成系统Bug. 下个阶段重点解决漏标问题.
重新标记
- 浮动垃圾 本轮不处理
- 漏标 漏标可能造成系统Bug,必须要解决的. 我们首先分析一下漏标是如何发生的.
漏标的充分必要条件
- 赋值器插入了一条或多条从黑色对象到白色对象的新引用
- 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
如何理解?或者说为什么?
- 在并发标记阶段,新生成的对象都是黑色. 所以漏标只需考虑老的对象.
- 条件1好理解: 白色对象只有被移动到了已经被标记为黑色的对象下才可能逃离被标记.
- 条件2包含两个意思:
a. 必须是全部灰色对象到改对象的引用. 如果仍存在只需一个到灰色对象的引用,随着扫描的进行,改白色对象必然会被扫描到.
b.条件1中白色对象之前必须被灰色对象直接或者间接引用; 为什么必须满足呢? 这一疑问可以转化为 条件1的白色对象在赋值前是否可能不在任何灰色对象的引用链下,即不在任何rc root的引用链下,即垃圾. 答案是否定的. 如果是垃圾对象,疑问着程序运行阶段无法直接获取到对象的引用.
漏标的解决
我们已经知道漏标的充分必要条件.那么只要我们破坏其一即可避免漏标的发生.- 增量更新: 破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了,CMS使用的是这种解决方案
-原始快照(STAB): 破坏的是第二个条件.当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索,G1使用的是这种解决方案. 这种方案可能产生浮动垃圾.
如何实现:
-写屏障 在赋值操作前后,加入一些处理.类似AOP
-读屏障 在读取一个对象,在赋值前就记录下引用关系
CMS:增量更新 VS G1:STAB- 猜测其他的想法是很难(stupid)的
- 增量更新因为需要重新深度扫描变更的关系,所以效率上比较低.
- STAB可能引入更多的浮动垃圾
- G1本身目标就非收集所有垃圾,而是可预测的时间内收集部分垃圾. 浮动垃圾不是问题. 从内存体量上讲, G1通常用于大内存. 深度扫描的成本比CMS更高.
并发清理
gc线程和用户线程并发. 这个时候所有新生成的对象及其引用都被标记为黑色. 引入更多浮动垃圾
3.4.4 CMS垃圾收集器缺点和问题
- 实现复杂
- 对CPU资源敏感(会和服务抢资源)
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
3.4.5 核心调优参数
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
3.5 G1收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征.
G1 目前接触较少,本章节暂不讨论.以后单独章节补充