JVM中常见的垃圾回收器
Serial/Serial Old
JVM刚诞生就只有这种最古老的、单线程、独占式、成熟、适合单CPU,一般用在客户端模式下。
这种垃圾回收器只适合几十兆到一两百兆的堆空间进行回收(可以控制停顿时间在100ms左右),但对于超过这个大小的内存回收速度很慢,所以对现在来说这个垃圾回收器已经是一个“垃圾”。
Stop The World(STW)
单线程进行垃圾回收时需要暂停所有工作线程,直到他回收结束。这个暂停时间叫Stop The World,但是这种STW带来了恶劣的用户体验,比如:应用运行一个小时需要暂停5分钟。这也是早期JVM和java被C/C++语言诟病性能差的一个原因。所以JVM开发团队一直努力消除或者降低STW的时间。
Parallel Scavenge(ParallerGC)/Parallel Old
为了提高回收效率,从JDK1.3开始,JVM使用了多线程垃圾回收机制,关注吞吐量的垃圾收集器,高吞吐量可以高效率的利用CPU时间,尽快完成程序运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行代码的时间/(运行用户代码的时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集话费1分钟,那吞吐量就是99%。
该垃圾回收器适合回收堆空间上百兆~几个G
参数设置
JDK1.8 默认就是以下组合:
-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old
收集器提供了两个参数用于精确控制吞吐量,分别控制的停顿时间的-XX:MaxGCPauseMillis
参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。
-XX:MaxGCPauseMillis
不要认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐 量和新生代空间为代价换取的:系统把新生代调得小一些,收集 300MB 新生代肯定比收集 500MB 快,但这也直接导致垃圾收集发生得更频繁,原来 10 秒 收集一次、每次停顿 100 毫秒,现在变成 5 秒收集一次、 每次停顿 70 毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio
XX:GCTimeRatio 参数的值则应当是一个大于 0 小于 100 的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
例如:把此参数设置为 19, 那允许的最大垃圾收集时占用总时间的 5% (即 1/(1+19)), 默认值为 99,即允许最大 1% (即 1/(1+99))的垃圾收集时间 由于与吞吐量关系密切,ParallelScavenge 是“吞吐量优先垃圾回收器”。
-XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy (默认开启)。这是一个开关参数, 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的 比例(-XX:SurvivorRatio)、 晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调 整这些参数以提供最合适的停顿时间或者最大的吞吐量。
Concurrent Mark Sweep (CMS)
目前很大一部分的java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来最好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面的集中收集器来说更复杂一些,整个过程包括四个步骤:
- 初始标记 -短暂,仅仅只是标记下GC Roots能直接关联到的对象,速度很快。
- 并发标记 -时间长,但是是和用户线程同时进行,进行GC Roots追踪的过程,标记从GCRoots开始相关所有对象开始遍历整个可达性分析的对象。
- 重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这各阶段的停顿时间一般会比初始标记阶段稍微长一些,但远比并发标记的时间短。
- 并发清除 清除标记的对象,跟用户线程同时进行。
由于整个过程耗时最长的并发标记和并发清除过程都是与用户线程一起工作,从总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
影响和存在的问题
CPU敏感
CMS对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足4个时,CMS对用户的影响较大。
浮动垃圾
由于并发清理阶段是同用户线程同时进行,伴随着程序运行自然就会有新的垃圾产生,这一部分垃圾出现在标记之后,CMS无法在当次收集清理他们,只好留待下一次GC在清理掉,这一部分垃圾叫浮动垃圾。
由于浮动垃圾的存在,因此需要预留一部分内存,意味着CMS收集不能像其他收集器那样等待老年代快满时再回收。
在 1.6 的版本中老年代空间使用率阈值(92%) 如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
产生空间碎片
标记 - 清除算法会导致产生不连续的空间碎片
总体来说,CMS 是 JVM 推出了第一款并发垃圾收集器,所以还是非常有代表性。 但是最大的问题是 CMS 采用了标记清除算法,所以会有内存碎片,当碎片较多时,给大对象的分配带来很大的麻烦,
为了解决这个问题,CMS 提供一个 参数:-XX:+UseCMSCompactAtFullCollection,一般是开启的,如果分配不了大对象,就进行内存碎片的整理过程。 这个地方一般会使用 Serial Old ,因为 Serial Old 是一个单线程,所以如果内存空间很大、且对象较多时,CMS 发生这样情况会很卡
CMS 总结
CMS 问题比较多,所以现在没有一个版本默认是 CMS,只能手工指定。但是它毕竟是第一个并发垃圾回收器,对于了解并发垃圾回收具有一定意义。 为什么 CMS 采用标记-清除,在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动(对象的移动必定涉及到引用的变化,这个需 要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长),所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。
该垃圾回收器适合回收堆空间几个 G~ 20G 左右。
Garbage First(G1)
设计思想
随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,但是如果按照传统的分代模型,总跳不出 STW 时间不可预测这点。 为了实现 STW 的时间可预测,首先要有一个思想上的改变。G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是 新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
Region
Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中还有一类特殊的 Humongous 区域,专门用来存储大对象。 G1 认为只要大小超过 了一个 Region 容量一半的对象即可判定为大对象。每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次 幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的进行回收大多数情况下都把 Humongous Region 作为老年代的一部分来进行看待。
运行过程
G1的运作过程大致可划分为四个步骤:
- 初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。 这个阶段需要停顿线程,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
TAMS 是什么?
要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的分配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针, 从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。 - 并发标记
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 - 最终标记
对用户线程做另一个短暂的暂停,用于处理并发阶段结后仍遗留下来的最后那少量的漏标对象。 - 筛选回收
负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构 成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动, 是必须暂停用户线程,由多条收集器线程并行完成的。
特点
并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿的时间,部分其他收集器 原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
分代收集:与其他收集器一样,分代概念在 G1 中依然得以保留。虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但它能够采用不同的方式 去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象以获取更好的收集效果。
空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复 制”算法实现的,但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运 行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
追求停顿时间: -XX:MaxGCPauseMillis 指定目标的最大停顿时间,G1 尝试调整新生代和老年代的比例,堆大小,晋升年龄来达到这个目标时间。
垃圾回收器整理