之前已经讲过了不少有关 GC 的内容, 今天准备将之前没有细讲的部分进行补充, 首先要提到的就是垃圾收集器.
基础的回收方式有三种: 清除, 压缩, 复制, 衍生出来的垃圾收集器有:
Serial 收集器
新生代收集器, 使用停止复制算法, 使用一个线程进行 GC , 串行, 其它工作线程暂停.
使用 - XX:+UseSerialGC 开关来控制使用 Serial + Serial Old 模式运行进行内存回收(这也是虚拟机在 Client 模式下运行的默认值).
ParNew 收集器
新生代收集器, 使用停止复制算法, Serial 收集器的多线程版, 用多个线程进行 GC , 并行, 其它工作线程暂停, 关注缩短垃圾收集时间.
使用 - XX:+UseParNewGC 开关来控制使用 ParNew + Serial Old 收集器组合收集内存; 使用 - XX:ParallelGCThreads 来设置执行内存回收的线程数.
Parallel Scavenge 收集器
新生代收集器, 使用停止复制算法, 关注 CPU 吞吐量, 即运行用户代码的时间 / 总时间, 比如: JVM 运行 100 分钟, 其中运行用户代码 99 分钟, 垃 圾收集 1 分钟, 则吞吐量是 99% , 这种收集器能最高效率的利用 CPU , 适合运行后台运算(其他关注缩短垃圾收集时间的收集器, 如 CMS , 等待时间很少, 所以适 合用户交互, 提高用户体验).
使用 - XX:+UseParallelGC 开关控制使用 Parallel Scavenge + Serial Old 收集器组合回收垃圾(这也是在 Server 模式下的默认值); 使用 - XX:GCTimeRatio 来设置用户执行时间占总时间的比例, 默认 99 , 即 1% 的时间用来进行垃圾回收. 使用 - XX:MaxGCPauseMillis 设置 GC 的最大停顿时间(这个参数只对 Parallel Scavenge 有效), 用开关参数 - XX:+UseAdaptiveSizePolicy 可以进行动态控制, 如自动调整 Eden / Survivor 比例, 老年代对象年龄, 新生代大小等, 这个参数在 ParNew 下没有.
Serial Old 收集器
老年代收集器, 单线程收集器, 串行, 使用标记 - 整理算法, 使用单线程进行 GC, 其它工作线程暂停(注意: 在老年代中进行标记 - 整理算法清理, 也需要暂停其它线程), 在 JDK1.5 之前, Serial Old 收集器与 ParallelScavenge 搭配使用.
整理的方法是 Sweep (清除)和 Compact (压缩), 清除是将废弃的对象干掉, 只留幸存的对象, 压缩是移动对象, 将空间填满保证内存分为 2 块, 一块全是对象, 一块空闲),
Parallel Old 收集器
老年代收集器, 多线程, 并行, 多线程机制与 Parallel Scavenge 差不错, 使用标记 - 整理算法, 在 Parallel Old 执行时, 仍然需要暂停其它工作线程.
Parallel Old 收集器的整理, 与 Serial Old 不同, 这里的整理是 Copy(复制)和 Compact(压缩), 复制的意思就是将幸存的对象复制到预先准备好的区域, 而不是像 Sweep(清除)那样清除废弃的对象.
Parallel Old 在多核计算中很有用. Parallel Old 出现后(JDK 1.6), 与 Parallel Scavenge 配合有很好的效果, 充分体现 Parallel Scavenge 收集器吞吐量优先的效果. 使用 - XX:+UseParallelOldGC 开关控制使用 Parallel Scavenge + Parallel Old 组合收集器进行收集.
CMS
全称 Concurrent Mark Sweep, 老年代收集器, 致力于获取最短回收停顿时间(即缩短垃圾回收的时间), 使用标记 - 清除算法, 多线程, 优点是并发收集(用户线程可以和 GC 线程同时工作), 停顿小.
使用 - XX:+UseConcMarkSweepGC 进行 ParNew + CMS + Serial Old 进行内存回收, 优先使用 ParNew + CMS(原因见后面), 当用户线程内存不足时, 采用备用方案 Serial Old 收集.
如何开始
首先来看一下 CMS 是在什么情况下进行 GC:
首先 JVM 根据
- -XX:CMSInitiatingOccupancyFraction
- ,
- -XX:+UseCMSInitiatingOccupancyOnly
来决定什么时间开始垃圾收集.
如果设置了
-XX:+UseCMSInitiatingOccupancyOnly
, 那么只有当老年代占用确实达到了
-XX:CMSInitiatingOccupancyFraction
参数所设定的比例时才会触发 CMS GC.
如果没有设置
-XX:+UseCMSInitiatingOccupancyOnly
, 那么系统会根据统计数据自行决定什么时候触发 CMS GC. 因此有时会遇到设置了 80% 比例才 CMS GC, 但是 50% 时就已经触发了, 就是因为这个参数没有设置的原因.
具体执行
CMS GC 的执行过程, 具体来说就是:
初始标记(CMS-initial-mark)
该阶段是 stop the world 阶段, 因此此阶段标记的对象只是从 root 集最直接可达的对象.
此阶段会打印 1 条日志: CMS-initial-mark:961330K(1572864K), 指标记时, 老年代的已用空间和总空间
并发标记(CMS-concurrent-mark)
此阶段是和应用线程并发执行的, 所谓并发收集器指的就是这个, 主要作用是标记可达的对象, 此阶段不需要用户线程停顿.
此阶段会打印 2 条日志: CMS-concurrent-mark-start,CMS-concurrent-mark
预清理(CMS-concurrent-preclean)
此阶段主要是进行一些预清理, 因为标记和应用线程是并发执行的, 因此会有些对象的状态在标记后会改变, 此阶段正是解决这个问题. 因为之后的 CMS-remark 阶段也会 stop the world, 为了使暂停的时间尽可能的小, 也需要 preclean 阶段先做一部分工作以节省时间.
此阶段会打印 2 条日志: CMS-concurrent-preclean-start,CMS-concurrent-preclean
可控预清理(CMS-concurrent-abortable-preclean)
此阶段的目的是使 CMS GC 更加可控一些, 作用也是执行一些预清理, 以减少 CMS-remark 阶段造成应用暂停的时间.
此阶段涉及几个参数:
-XX:CMSMaxAbortablePrecleanTime: 当 abortable-preclean 阶段执行达到这个时间时才会结束.
-XX:CMSScheduleRemarkEdenSizeThreshold(默认 2m): 控制 abortable-preclean 阶段什么时候开始执行, 即当年轻代使用达到此值时, 才会开始 abortable-preclean 阶段.
-XX:CMSScheduleRemarkEdenPenetratio(默认 50%): 控制 abortable-preclean 阶段什么时候结束执行.
此阶段会打印 3 条日志: CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,CMS:abort preclean due to time XXX
重新标记(CMS-remark)
此阶段暂停应用线程, 停顿时间比并发标记小得多, 但比初始标记稍长, 因为会对所有对象进行重新扫描并标记.
此阶段会打印以下日志:
YG occupancy:964861K(2403008K), 指执行时年轻代的情况.
CMS remark:961330K(1572864K), 指执行时老年代的情况.
此外, 还打印出了弱引用处理, 类卸载等过程的耗时.
并发清除(CMS-concurrent-sweep)
此阶段进行并发的垃圾清理.
并发重设状态等待下次 CMS 的触发(CMS-concurrent-reset)
此阶段是为下一次 CMS GC 重置相关数据结构.
总结
CMS 的收集过程, 概括一下就是: 2 次标记, 2 次预清除, 1 次重新标记, 1 次清除.
在 CMS 清理过程中, 只有初始标记和重新标记需要短暂停顿用户线程, 并发标记和并发清除都不需要暂停用户线程, 因此效率很高, 很适合高交互的场合.
CMS 也有缺点, 它需要消耗额外的 CPU 和内存资源. 在 CPU 和内存资源紧张, 会加重系统负担(CMS 默认启动线程数为( CPU 数量 + 3 ) / 4 ).
另外, 在并发收集过程中, 用户线程仍然在运行, 仍然产生内存垃圾, 所以可能产生 "浮动垃圾"(本次无法清理, 只能下一次 Full GC 才清理). 因此在 GC 期间, 需要预留足够的内存给用户线程使用.
所以使用 CMS 的收集器并不是老年代满了才触发 Full GC , 而是在使用了一大半 (默认 68% , 即 2/3 , 使用 - XX:CMSInitiatingOccupancyFraction 来设置) 的时候就要进行 Full GC. 如果用户线程消耗内存不是特别大, 可以适当调高 - XX:CMSInitiatingOccupancyFraction 以降低 GC 次数, 提高性能. 如果预留的用户线程内存不够, 则会触发 Concurrent Mode Failure, 此时, 将触发备用方案: 使用 Serial Old 收集器进行收集, 但这样停顿时间就长了, 因此 - XX:CMSInitiatingOccupancyFraction 不宜设的过大.
还有, CMS 采用的是标记 - 清除算法, 会导致内存碎片的产生, 可以使用 - XX:+UseCMSCompactAtFullCollection 来设置是否在 Full GC 之后进行碎片整理, 用 - XX:CMSFullGCsBeforeCompaction 来设置在执行多少次不压缩的 Full GC 之后, 来一次带压缩的 Full GC.
并发和并行
并发收集:
指用户线程与 GC 线程同时执行(不一定是并行, 可能交替, 但总体上是在同时执行的), 不需要停顿用户线程(其实在 CMS 中用户线程还是需要停顿的, 只是非常短, GC 线程在另一个 CPU 上执行);
并行收集:
指多个 GC 线程并行工作, 但此时用户线程是暂停的;
所以, Serial 是串行的, Parallel 收集器是并行的, 而 CMS 收集器是并发的.
总结
今天了解了一下普通的垃圾收集器, 并且详细介绍了 CMS, 其特性其实是基于普通的垃圾算法, 增加了预处理, 预清除的过程, 因此效率更加优越. 当然它也有自己的缺点, 更加消耗资源, 因此在选用的时候需要结合实际场景.
有兴趣的话可以访问我的博客或者关注我的公众号, 头条号, 说不定会有意外的惊喜.
https://death00.github.io/
来源: https://www.cnblogs.com/death00/p/11774923.html