垃圾收集器
JVM(十):垃圾收集器
在上文中,我们对不同的算法实现进行了分析。在本文中,我们就对其算法的具体实现——垃圾收集器进行分析对比。
总览
垃圾收集器依赖于具体的虚拟机实现。在本文中,我们采取的是 HotSpot,其内置了 7种类型的垃圾收集器实现。下面就让我们逐个进行学习,分析。
Serial
Serial,最基本最老的收集器。其存在的主要问题是”Stop The World”,即在垃圾回收的时候,必须要暂停所有的其他工作线程,直到收集完成。此外在工作的时候,其垃圾收集线程是单线程的。
ParNew
ParNew 其实就是 Serial 的多线程版本。其除了垃圾收集线程是多线程意外,其他功能与 Serial 一样,如收集算法、Stop The World 的特性、对象分配规则、回收策略等。
Parallel Scavenge
Parallel Scavenge 大多数特性与 ParNew 相似,其最主要的特点是其设计的目标是达到一个 可控制的吞吐量,所谓吞吐量就是 CPU 用于运行用户代码的时间和 CPU 总消耗时间的比值,即 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。高吞吐量意味着高效率地利用 CPU 时间,尽快完成程序计算任务,适合在后台运算而不需要太多交互的任务。
Serial Old
其是 Serial 的老年代版本,其他特性和 Serial 类似,不多赘述。
Parallel Old
其是 Parallel Scavenge 的老年代版本,适合跟 Parallel Scavenge 一起组成一个 吞吐量优先 的收集组合。
CMS
CMS的全称为 Concurrent Mark Sweep,其设计理念是获取最短回收停顿时间。其回收步骤为
- 初始标记
- 并发标记
- 重新标记
- 并发清除
初始标记是简单标记一下 GC Roots 能关联到的对象,因此速度很快。并发标记则需要进行 GC Roots Tracing,重新标记是为了修正在并发标记期间因用户程序继续运行而导致标记产生变动的对象,最终执行并发清除过程。
在收集过程中,只有初始标记和重新标记需要 Stop The World, 耗时较多。而耗时较多的并发标记和并发清除阶段都是可以和用户线程一起执行的,因此其可以大大减少停顿时间。
从CMS的执行模型中,我们也可以看到其拥有以下缺点。
首先我们看到整个 GC 在第 4步是并发清除的,那么在清除过程中就会产生 浮动垃圾 ,因为此时用户线程还在继续执行,因此必然会产生新的垃圾,而这些垃圾因为已经错过了这次的标记,就需要等到下次才能被回收。
此外也正是因为并发清除的原因, JVM 需要为用户运行过程产生的对象预留空间,那么这个度又该如何把握呢?如果预留的少了,就会出现 “Concurrent Mode Failure” 失败,此时 JVM 会启用后备方案:Serial Old,那么就会导致较长时间的停顿。如果预留的多了,那么就会频繁地进行 Full GC,影响性能。
并且因为清除阶段采用的是并发清除,那么用户线程因为在执行的原因,就必然只能采用 标记-清除算法 ,这样就会导致产生大量的内存碎片。
最后因为 CMS 是一个针对并发模型的实现,那么在并发阶段就必然会占用 CPU 时间,这就必然会导致应用程序变慢,因为这个是所有针对并发进行设计的程序都会有的问题。
G1
Garbage-First 算法的核心观念是将堆划分为多个大小相等的独立区域——Region(还保留着新生代和老年代,但其不再是物理隔离的,而是一系列region的集合)。
在程序运行过程中,G1 会维护一个 优先级列表,里面记录着各个 Region 的垃圾堆积价值(回收所获得大小和回收所需时间的比值),这样在每次回收的时候,根据 允许的回收时间,优先回收价值最大的 Region,通过这样,我们的 GC 时间就是可预测的。
但是听着将堆划分为多个 Region 很简单,在实际情况下,我们该如何保证 Region 以及其中对象的独立性呢?
实际上,Region 不可能是独立的,因为一个对象分配到这个 Region 上,但其可能会被堆上的任一个引用所关联。这样在进行可达性分析的时候,难道意味着要进行 全堆扫描 吗?如果这样的话,性能又该如何保证呢?
因此 G1 使用了 Remembered Set 来避免这一现象。在程序对 Reference 类型数据进行写操作时,其会产生一个 Write Barrier 暂时中断写操作,来检查 Reference 引用对象是否处于不同的 Region 中,如果是,则通过 CardTable 将相关引用信息记录到对象所属 Region 的 Remembered Set 中,这样在进行内存回收,可达性分析的时候通过 Remembered Set 则可以保证不对全堆扫描也不会有遗漏。
G1 详细的执行流程如下:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
粗略来看,G1 的前 3个步骤和 CMS 十分类似。但其内部还是有些不一样。
首先是初始标记阶段,G1 和 CMS 类似,也只是标记一下 GC Roots 能直接关联到的对象,但其还修改了 TAMS(Next Top at Mark Start)的值,让下一阶段并发标记时,用户线程可以正确的在可用 Region 中创建对象。并发标记阶段和 CMS 一样,但将这段时间对象变化记录存储在 Remembered Set Log 中;在最终标记阶段,将 Remembered Set Log 合并到 Remembered Set 中,最终在筛选回收阶段首先对 各个 Region 的价值进行排序,然后根据用户设置的回收时间来制定回收计划。
GC 常用参数
总结
在本文中,我们根据不同的算法,分析了 Hotspot 中存在的 7种垃圾收集器的具体实现,分析其在使用过程中的优劣,明白每个收集的设计理念,以及其代表的思想。
最后,给了一个在垃圾收集过程中常用的参数,在使用过程中开发者需要根据实际情况进行实时调节,才能达到最大的性能。
文章在公众号 “iceWang” 第一手更新,有兴趣的朋友可以关注公众号,第一时间看到笔者分享的各项知识点,谢谢!笔芯!
本系列文章主要借鉴自《深入分析 JavaWeb 技术内幕》和《深入理解 Java 虚拟机-JVM高级特性与最佳实践》。