理解 JVM 工作机制(六) 垃圾收集器
垃圾收集器
来明确一个观点:虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。
JDK8 环境下,默认使用 Parallel Scavenge + Parallel Old
展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器
Stop The World
停止所有用户线程,称为“Stop The World”
新生代收集器(Minor GC)
Serial 收集器
客户端模式下的默认收集器(桌面应用)
当收集器开始工作时,会暂停其他所有工作现场,直到它收集结束
该收集器消耗内存小,对于单核处理器或处理器核心数较少的环境来说,没有线程交互的开销。即使在这段时间停止一切用户线程,也不会感觉明显卡顿。适用与客户端使用
ParNew 收集器
Serial收集器的并行版本
ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。
它是 JDK1.7 之前首选的新生代收集器,除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
Parallel Scavenge 收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器
Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:
Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高。单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。
通过参数 -XX:MaxGCPauseMillis: 最小回收内存花费的时间,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的。
通过参数 -XX:GCTimeRadio:垃圾收集时间占总时间的百分比,假设 19,那么即 %5 (1/(1+19))
通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器会自动调整新生代的大小、Eden 和 Survivor 的比例、对象进入老年代的年龄,以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio。
老年代收集器(Major GC)
Serial Old 收集器
Serial收集器的老年版本,使用标记-整理算法
用途:
JDK5 以前和 Parallel Scavenge 搭配使用
CMS 收集器失败后的备用方案
Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,可以充分利用多核CPU的计算能力。
CMS 收集器(Concurrent Mark Sweep)
追求最短回收停顿时间,基于标记清除算法实现,整个过程分为四个步骤
初始标记(CMS initial mark) 单条线程标记所有 GC Roots 能直接关联到的对象
并发标记(CMS concurrent mark) 使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢
重新标记(CMS remark) 修正在并发标记中那些因为用户线程继续运作修改的对象
并发清除(CMS concurrent sweep) 清理删除掉标记阶段判断的已经死亡的对象
并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS 的缺点:
吞吐量低
无法处理浮动垃圾,导致频繁 Full GC
使用“标记-清除”算法产生碎片空间
对于产生碎片空间的问题,可以通过开启 -XX:+UseCMSCompactAtFullCollection,在每次 Full GC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块。设置参数 -XX:CMSFullGCsBeforeCompaction 告诉 CMS,经过了 N 次 Full GC 之后再进行一次内存整理。
混合收集器(Mixed GC)
G1 收集器
Mixed GC, JDK9之后取代了Parallel Scavenge 和 Parallel Old的组合,CMS也被官方声明为弃用
G1 是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
从整体上看, G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
Region 的堆内存布局
Region 大小指定: XX:G1HeapRegionSize 1~32MB,且为 2 的 N 次幂
优先处理回收价值收益最大的那些 Region
收集停顿时间
I-XX:MaxGCPauseMillis
单个空间大小
过程:
初始标记。标记出 GC Roots 直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
并发标记。从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
最终标记。修正在并发标记阶段引用户程序执行而产生变动的标记记录。
筛选回收。选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First ,第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。
适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用 -XX:+UseG1GC 使用 G1 收集器,JDK9 默认使用 G1 收集器
特点
并行与并发。G1 能充分利用多 CPU,多核环境下的硬件优势。
分代收集。能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
空间整合。G1 从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的,因此 G1 运行期间不会产生空间碎片。
可预测的停顿。G1 能建立可预测的时间停顿模型,能让使用者明确指定一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
如何解决跨代引用对象?
为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个 Remembered Set 来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。
各款收集器的并发情况
版权声明: 本文为 InfoQ 作者【ue4】的原创文章。
原文链接:【http://xie.infoq.cn/article/1c270b5e60d1214832a6866c8】。文章转载请联系作者。
评论