JVM 垃圾回收器总结

465 阅读10分钟

在说垃圾回收器之前先复习一下一些概念,以便能更形象的知道各个收集器的设计目标及特性。

一.先回顾一下分代收集理论(毕竟hotSpot以此为基础进行收集的)

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论去设计的。也就是将堆分为“新生代”与“老年代”。一个在新生代的对象经过一次次GC 不断的改变着自己对象头上的GC年龄 直至进入老年代,当然新生代进行收集的时候98%基本都会被清除。(简单带过 具体的自己去看去吧)

image.png

1.部分收集(Partial)

1.1 新生代收集(Minor GC / Young GC)

指目标只是新生代的垃圾收集

1.2 老年代收集(Major GC / Old GC)

只发生在老年代的GC,目前就只有CMS收集器会有单独收集老年代的行为

1.3 混合收集(Mixed GC)

指目标是收集整个新生代以及部分老年代的老年代的垃圾收集,目前只有G1收集器会有这种行为

2.整堆收集(Full GC)

收集整个Java堆和方法区的垃圾收集器

二.可达性分析的实际应用以及并发的可达性分析

2.1 一致性快照及垃圾收集器为之努力的其中一个目标

当前主流的垃圾收集器基本都是依靠可达性分析算法来判定对象是否存活的,可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能进行分析,这就意味着要冻结用户线程的运行。由于GC Root的个数和整个堆中对象比起来还是占据极少数,并且在OopMap等优化的加持下,它带来的停顿时间已经是非常短暂且固定(不会随堆容量而增长),但是!!!从GC Root往下进行遍历对象图,这的时间就会和堆的容量成正比了,于是为了减少这部分的时间便成了各个虚拟机为之努力的目标(除了 PS收集器 它追寻的是吞吐量)

2.2 为什么一定要一致性的快照?---三色标记法

要想解决或降低用户线程的停顿时间,就必须要搞清楚为什么必须在一个能保障一致性的快照上才能进行对象图的遍历? 再遍历对象图的时候,我们可以把遇到的对象,按照“是否访问过”这个条件进行标记成“黑”,“灰”,“白”三种颜色

白色:表示对象尚未被垃圾收集器访问过。显然在刚开始分析的时候都是白色,如果分析完了,仍然是白色,即代表不可达,这些白色就可以被打扫掉了。
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有饮用都已经扫描过。黑色的代表是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能不经过灰色直接指向白色对象
灰色:表示对象已经被垃圾收集器访问过,但这个对象至少还存在一个引用还没有被扫描过。

上图描述

2.2.1 初始状态,只有GC Root 是黑色的,对象只有被黑色对象引用才会存活,其它的都会消亡 image.png

2.2.2 扫描过程中,推进式扫描,以灰色为中间介质 由黑向白推进 image.png

2.2.3 扫描完成,此时黑色就是可以存活的,白色的就是可以被清除的 image.png

2.3 并发的可行性分析

了解了上面的几个概念,那我们改如何优化“冻结用户线程”这个问题呢? 我们是不是可以GC 线程进行工作的同时让用户线程也可以继续工作呢?在得到结论前 我们不妨来想一下如果并发进行会出现的问题。

问题一:把原本消亡的对象错误的标记为存活 如图在推进过程中正在(ing)扫描的灰色的对象的引用被黑色饮用 image.png

问题二:把该活的对象标记死了 如图切断后重新被黑色引用,由于黑色不会重新扫描,所以导致最后扫描完成这个新的引用的对象仍是白色 会被清除掉 这是不应该的

image.png

其中第一个问题是可以容忍的,但是第二个问题(误删) 我们该怎么解决呢?

1.增量更新 当黑色对象插入新的指向白色对象的引用关系时,会将这个新插入的引用记录下来,等并发扫描结束后,再将这些记录的引用关系中的黑色对象为根再重新扫描一次 (记录新插入黑色和白色的引用关系,之后以关系中的黑色为root再扫,黑退化为灰) (CMS)

2.原始快照 当灰色对象要删除指向白色对象的引用关系时,就把这个删除的记录下来,在并发扫描结束后,再以记录下来的关系中的灰色为Root重新扫描(记录灰色和白色的引用关系,之后再以灰色为Root扫,相当于记录下了那一时刻的快照) (G1,Shenandoah)

以上两种方式的引用关系记录都是通过“写屏障”来完成的

三.经典的垃圾收集器

找了一张对比图 感谢水印大佬

image.png

Serial

作用于年轻代 标记复制 虽然是单线程的 但是在单核处理器中或者处理器核心数较少的环境中,Serial由于没有线程交互的开销,倒是可以高效率的运行 可以和SerialOld 和CMS一起使用 低内存下可以考虑使用 但是现在很少使用了

image.png

ParNew

作用于年轻代 标记复制 多线程并行进行垃圾收集 除了线程其余基本上完全等同于Serial 可以与SerialOld 和CMS搭配使用 自JDK9开始官方不推荐使用ParNew + SerialOld以及 Serial + CMS了 ParNew在单线程里绝对不会比Serial表现的好因为会有多线程的开销 不过在多核环境下表现还ok

image.png

Parallel Scavenge(PS)

作用于年轻代 标记复制 多线程并行进行垃圾收集 但是他追求的不是用户线程停顿的时间,追求的是吞吐量,所以适合应用于纯后台计算的服务而不是和前台用户交互的服务中 可以于Parallel Old搭配使用 吞吐量=运行用户代码时间/运行用户代码时间+运行垃圾收集时间 可以通过-XX:GCTimeRatio(设置吞吐量大小) 大于0小于99 -XX:MaxGCPauseMillis(控制最大垃圾收集时间) 大于0 的毫秒数 控制吞吐量 也可通过-XX:+UserAdaptiveSizePolcy 让系统自己控制新生代Eden 与Survivor区的比例 以及晋升老年代对象的大小等参数 系统动态调节这些参数(自适应策略)来提升吞吐量

Parallel Old(PO)

作用于老年代 标记整理 多线程并发收集 自JDK 6时一直搭配着PS使用 保证吞吐量

image.png

CMS

作用于老年代 标记清除 多线程并发收集 追求的是最短回收停顿时间 适用于B/S服务 及时响应客户端请求 CMS整个收集过程分为4步

1.初始标记 (Stop The World)

2.并发标记

3.重新标记 (Stop The World)

4.并发清除

缺点:

  • 虽然并发阶段不会暂停用户线程,但是新开出的GC线程也是要占用资源的,可能会导致程序变慢,降低总吞吐量,CMS默认开启的回收线程数是(处理器核心数量+3)/4

  • 在并发标记和并发清理阶段,因为不STW,所以用户县城还是正常运行的,程序就自然伴随着新的垃圾对象产生,但是这一部分是在标记完成后出现的,所以当次无法回收,这一部分垃圾称为“浮动垃圾”,所以CMS要预留出一定的空间给新的对象,这个时候可能面临一个风险,就是来了一个大对象,预留空间因无法分配内存,就会出现一次“并发失败”,这个时候就不得不启动预案:Serial Old重新进行收集这个时候 时间就长了 所以我们应该避免这种状况 可以适当的调小一点 -XX:CMSInitiatingOccupancyFraction (触发百分比)但是太小了也不行,太小了就总会触发GC

image.png

Garbage First(G1)

目标:停顿时间模型(支持指定一个长度为M毫秒的时间片段,消耗在垃圾收集上的时间大概率不会超过N这样的目标)

分为4步:

  • 初始标记(Stop The World)

  • 并发标记

  • 最终标记(Stop The World)

  • 筛选回收 (Stop The World)

分代:作用于年轻代+老年代,但是它的分代并没有严格的物理界限 标记整理+标记复制 多线程并发 追求同CMS 适用于面向服务端的应用 保证响应时间

G1不直接面向老年代或者年轻代进行收集,而是面向堆内任何部分组成回收集(CSet)进行回收,衡量标准不再是属于哪个代,而是哪块内存存放的垃圾最多,回收效益最大,这就是Mixed GC模式

G1把连续的Java堆划分为多个大小相等且独立的区(Region)每个Region都可以根据需要扮演Eden空间或者Suivivor空间,或者是老年代空间,之后收集器能根据扮演角色的不同去用不同的策略处理,可根据 -XX:G1HeapRegionSize设定(1M~32M)一般是2的N次幂 Region中还有一个比较特殊的Humongous区域专门用来存放大对象的(大小超过Region的一半),会被当作老年代看待

G1虽然仍保留了新生代老年代的概念,但是位置不在是固定的了,他们是一系列区域的集合(可以不连续)也就是说会把这些Region放到一个优先级列表之后根据列表进行垃圾回收

缺点:

  • G1也存在跨代问题,也是用卡表维护关系,但是它的卡表不是数组,本质上是一个Hash表,key是别的Region的起始地址,value是一个集合,存着卡表的索引号(也就是说hash里不仅存着我指向谁,还要存着谁指向我)为什么这么存呢:预防追溯不到

  • 并发时问题2的解决G1是通过原始快照解决的,也就是存在着新对象内存分配问题,如果内存回收速度赶不上内存分配速度G1也要被迫冻结用户线程执行从而导致Full GC而产生长时间STW。

ZGC 在jdk11中,即将迎来ZGC(The ZGarbage Collector),这是一个处于实验阶段的,可扩展的低延迟垃圾回收器。

  • 每次GC STW的时间不超过10ms

  • 能够处理从几百M到几T的JAVA堆

  • 与G1相比,吞吐量下降不超过15%

image.png

四.如何选用合适的垃圾收集器呢

三选二原则

1.吞吐量 2.延迟 3.内存占用 任选两个去选择对应的

2.根据自己业务去实际进行压测

引用:深入理解JVM虚拟机 周志明