评价GC策略指标
- 以下指标评价GC处理器优缺点
- 吞吐量:系统吞吐量. 业务线程耗时/系统总时间
- 垃圾回收器负载:GC线程总耗时 / 业务线程总耗时 的比值
- 停顿时间:总的stop the word 时间
- 回收效率:GC在线程在完成垃圾对象标记后,多久能清理完
- 反应时间:指当一个对象确认为垃圾后,多长时间内,他所能被清理
- 堆内存分配:不同垃圾收集器内存分配不同,好的收集器应该有一个合理的分配方式。
新生代串行收集器(Serial)
- JDK中最老的也是最基本的回收器,两个特点
- 使用单线程进行垃圾回收
- 第二他是独占式回收
- 此垃圾回收器工作必然导致stop the world,但是他说一个最成熟的且高性能回收器,适用于新生代使用复制算法逻辑简单
- JVM参数设置 -XX:+UseSerialGC来指定新生代中,老年代中使用串行收集器,在堆内存空间较小时候更有优势,速度快毫秒级
老年代串行收集器(Serial Old)
- 老年代不同在于使用标记压缩算法,同样是单线程独占式,因此同样需要stop the word,但是可以和多种新生代回收器配合使用,如JVM参数
- -XX:UserSerialGC:新生代,老年代都用串行
- -XX:UserParNewGC:新生代用并行收集器,老年代使用串行收集器
- -XX:UseParallelGC:新生代用并行回收收集器,老年的用串行回收器
并行收集器(ParNew)
- 只是简单的将串行收集器多线程化,他的回收策略,算法,参数都和串行回收器一样,也是独占说,也需要stop the word
- 但是并行收集器多线程回收在多CPU情况下,停顿时间更短,在单个CPU下比串行回收更弱因为多线程有多余的上下文切换。
- 用以下参数开启并行回收器:
- -XX:+UserParNewGC:新生代使用并行收集器,老年代用串行收集器
- -XX:+UserConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS收集器
- 并行收集器线程数可设置,一半是CPU数,当CPU数超过8 ,则用3+[(5*cpu)/8]
新生代并行回收(Parallel Scavenge)收集器
- 同样用复制算法,他和并行收集器一样,都是多线程,独占式收集器,并且关注点在吞吐量:
- 用以下参数开启:
- -XX:+UseParallelGC :新生代使用并行回收收集器,老年代使用串行回收收集器
- -XX:+UseParallelOldGC:新生代和老年代都使用并行回收处理器
- 关键点:
- 使用 -XX:MaxGCPauseMillis 设置最大GC停顿时间,GC会自动调节堆大小达到这个目的,例如设置很小,则必然让堆内存减少才能减少GC时间,这时候GC频次增加,吞吐量下降
- 使用 -XX:GCTimeRatil 设置吞吐量,例如n,则系统话费不超过 1/(1+n)默认是99 也就是1%
- 使用 -XX:+UseAdaptiveSizePolicy 设置自适应GC,这种模式下类似全自动,新生代,eden和survivor比例,晋升老年代堆年龄等参数会自动调整,以达到,堆大小和吞吐量的平衡点,只要设置JVM的最大堆代奥,和这三个参数,其他的都会自动完成调优
老年代并行回收收集器(Parallel Old)
- 同样是一种多线程的收集器,但是用的是标记压缩算法,同样关注点在吞吐量,JDK1.6 中才引入
- 使用-XX:UseParallelOldGC 可以在新生代和老年代都使用并行回收收集器
- 使用-XX:parallelGCThreads 设置线程数量
CMS收集器
- CMS(Concurrent Mark Swap)并发标记清除,使用标记清除算法,同时也使用多线程进行垃圾回收
- 步骤如:
- 初始标记:首先用单线程对堆中对象进行快速标记(独占式)
- 并发标记:与业务线程一起进行对象标记
- 重新标记:为修正上一阶段业务线程新产生的垃圾,重新在标记一次(独占式)
- 并发清除:与业务线程并行对已标记的对象进行清除
- 并发重置:此处是重置CMS的数据结构,准备下一次GC
- CMS默认线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是新生代并行收集器的线程数,同时也可以手动手指CMS线程,合理设置用来均衡在CPU资源紧张的时候减少对业务线程的影响
- 通过使用-XX:CMSInitiatingOccupancyFraction来指定,默认68,也就是当老年代空间使用率到68%就开始GC,这样设置目的在于:需要保证留有足够空间在GC过程中也能同时运行业务线程
- 空间碎片问题:可以通过-XX:+UseCMSCompactAtFullCollection开关让CMS在GC后进行一次碎片整理,同样-XX:CMSFullGCsBeforeCompaction参数可以用于设置多次GC后在进行一次内存压缩,独占式的整理
- -XX:+UserConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS收集器
- CMS主要目的在于降低停顿时间
G1收集器
G1内存结构 Region
- G1 的内存结构和传统内存空间划不同,G1 将内存分为默认 1M ~ 32MK的多个Region,逻辑上连续,物理上不连续,每个Region被标记为E,S,O,H,分别是Eden,Survivor,Old,Humongous。E,S年轻代,O,H,老年代
G1的GC模式
Young GC
- Young GC回收的是所有年轻代的Region,当E区不能在分配新的对象时候会触。
- 使用复制算法,E区域移动到S区,S区域空间不够直接到O区域
- 同时S区域数据移动到新的S区域
- 如果S区域部分对象到一定的年龄,直接到O区域
Mixed GC
-
Mixed GC叫混合GC,他回收所有年轻代 + 部分老年代
-
什么时候触发 + 为什么是部分老年代?
-
回收老年代参数是 -XX:MaxGCPauseMillis 用来制定一个G1 收集过程的目标停顿时间默认 200ms,G1 有一个停顿预测模型,他会有选择的挑选部分Region来做GC,以此来满足停顿时间的要求
-
Mixed GC 触发节点也是参数控制:XX:InitiatingHeapOccupancyPercent 表示老年代占整个堆大小的百分比,默认45%,达到这个阀值 就触发一次Mixed GC
-
Mixec GC分为2个阶段:
-
第一个阶段:全局并发标记,可以细分为以下几个步骤
- 初始标记:独占式,STW,从GC Root开始找到可访问到的堆。完成初始标记阶段
- 并发标记:这个阶段从GC Root开始堆堆中对象标记, 标记线程与应用线程并行执行,收集各个Region存活对象信息
- 最终标记:独占式,STW,标记那些在并发标记阶段发生变化的对象,将被回收
- 清除垃圾:部分STW,这个阶段如果发现完全没有存活对象的Region,会将其整体会收到Region列表中,清除空的Region
-
第二个阶段:拷贝存活对象
- 全暂停阶段STW,负责将一部分Region里面存活的对象拷贝到空的Region里面(并行拷贝),然后回收原来的Region空间,本阶段可以自由选择任意多个Region来独立收集,被选择的Region构成一个集合,collection Set,集合中的Region的选定由停顿预测模型来决定,该阶段并不会处理所有的Region,因为时间可能不允许,选择高收益的少量Region。
- 全暂停阶段STW,负责将一部分Region里面存活的对象拷贝到空的Region里面(并行拷贝),然后回收原来的Region空间,本阶段可以自由选择任意多个Region来独立收集,被选择的Region构成一个集合,collection Set,集合中的Region的选定由停顿预测模型来决定,该阶段并不会处理所有的Region,因为时间可能不允许,选择高收益的少量Region。
Full GC
- G1 的垃圾回收过程是和应用程序并发执行的。当Mixed GC的速度,赶不上应用程序申请内存的速度时候,Mixed G1 会降级到Full GC,使用的是Serial GC(新生代,老年代都用串行收集)这种Full GC是单线程独占式的,会导致长时间的STW
- 导致G1 Full GC的原因可能有以下两种
- 拷贝存活对象时候,没有足够的to-space区域(可能是S或者O或者H)来存放晋升的对象
- 并发处理过程完成之前堆内存被耗尽
SATB & incremental Update
- 问题由来,在并发标记阶段,GC线程与 应用线程同时对对象进行修改,此时可能出现漏标记对象的情况。
- SATB 全称 Snapshot-At-The-Begining,由字面意思就是GC开始时候存活的对象快照
- 根据三色标记法,白色:没标记,灰色:标记了自身,但是子引用没标记,黑色:自身以及子引用都已标记
- 当并发标记阶段有如下问题出现,D引用由A转到C,那么以后都无法被扫到而被删,CMS与GC解决方式如下
- CMS中使用Incremental Update 关注的是引用的增加,此时C引用增加了,所以CMS 将C对象标记为灰色,下次重新扫描属性,这个地方其实是需要重新扫描灰色节点,这里会有性能上的问题,也会搜索其他的节点。
- G1 使用SATB,在删除A— > D 的引用时候,此时会在GC引用栈中添加一个D,在再次标记的时候会重新扫面引用栈中的对象以此来重新标记,此处一不会修改父节点的状态,二不会引起灰色节点的重新便利,所以比CMS的更高效
G1 的RSet,CSet
- 是用来辅助GC过程的一个数据结构,空间换时间的做法
- RSet全称Remembered Set,和Card Table类似,Rset记录了其他Region中对象到本Region中对象的关系
- CSet全称Collection Set 记录了GC要收集的Region的集合,并且可以是任意年代
- 这样对于old --> young 和old --> old 的引用就不用扫描整个old区域,直接看Rse,Cset这个两个集合中数据即可
- 例如标记的时候,只需遍历Cset找到Region,让后通过Rset找到引用关系即可,不需要便利整个堆提高效率
G1 与 CMS对比
-
CMS
- 优点:并发收集,低停顿
- 缺点:
- CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾(并发清除的时候产生的垃圾)
- CMS收集器是基于标记清除算法,会有内存碎片(虽然可以通过参数设置定时清理,但是STW)
- 停顿时间不可控
-
G1
- 优点:
- 并行与并发,利用多CPU缩短STW时间
- 分代收集
- 空间整合:标记整理算法,没有碎片
- 可以预测停顿时间
- G1不再只正对年轻代或者老年代,整改堆都用同一个处理
-
详细对比表
特征 | G1 | CMS |
---|---|---|
并发和分代 | 是 | 是 |
最大化释放堆内存 | 是 | 否 |
低延时 | 是 | 是 |
吞吐量 | 搞 | 低 |
压实(碎片) | 是 | 否 |
可预测性 | 强 | 弱 |
新生代和老年代的物理隔离 | 否 | 是 |
什么时候用G1
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间比较长
ZGC
- JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了,会分为一个个page,当进行GC操作时对page进行压缩,因此没有碎片问题
- ZGC只能在64位操作系统上使用
- ZGC支持TB级别内存
- 堆内存变大后同样能做大停顿时间在10ms以内
案例分析
//线上案例. zhenai-advertising-api
#!/bin/bash
/usr/bin/java -cp /data/dubbo/zhenai-advertising-api/classes:/data/dubbo/zhenai-advertising-api/libs-zhenai/*:/data/dubbo/zhenai-advertising-api/libs/*
-Xms2329m //最小堆 2.27G
-Xmx2329m //最大堆 2.27G
-Xss512k //线程栈大小512k
-XX:MetaspaceSize=256m //方法区初始值
-XX:MaxMetaspaceSize=256m //方法区最大值
-XX:+UnlockExperimentalVMOptions //docker配置
-XX:+UseCGroupMemoryLimitForHeap //docker配置
-Xmn1164m //新生代大小
-XX:+UseConcMarkSweepGC // 新生代使用并行收集器,老年代使用CMS收集器
-XX:+CMSParallelRemarkEnabled //采用并行标记方式降低停顿(默认开启)。
-XX:+UseCMSInitiatingOccupancyOnly //让下一个老年代比例配置生效
-XX:CMSInitiatingOccupancyFraction=70 //代表老年代堆空间的使用率,默认值为68,此处70表示达到70%使用率开始GC
-
如上配置中 使用的是PreNew+CMS 配合的垃圾收集算法,堆内存最大最小分别配置的-Xms =2329m -Xmx=2329,都是2.3G,因此他的年轻代的大小占用大概是 2329*30% = 700MB
-
项目特点,请求量大每天1.5亿,但是主要用来做数据收集,数据归因等操作,逻辑不难,并且不需要非常高的响应,强调的是吞吐量
-
在项目初期,发现在并发高峰期会有存在MQ消息堆积的问题,导致数据回传无法完成归因逻辑,从而导致数据匹配的不准确,因此开始研究了JVM的参数配置问题
- 首先查看了一下GC日志,发现并发高峰时候会有频繁的PreNew的MinoGC并且每次GC后剩下1%都不到,并且MinoGC的频率比较大,因此我们增加了堆内存的大小,扩容到了5G,并且将新生代老年代的比例设置为 1:1
- 接着调整了GC算法,之前用PreNew虽然能并发的利用多CPU,但是显然对于频繁的MinoGC下每次都需要STW,实际的吞吐量是降低的,因此我们将PreNew+CMS的组合替换成了G1 ,虽然在这种情况下一般都不会用到MixGC体现不出优势,但是相对于PreNew 而言G1 的YoungGC能利用并行回收的优势提升吞吐量,并且G1的停顿预测模型能够对YoungGC的停顿时间起到一个优化的作用。
- 接着协调运维那边接入了对项的监控和报警,以保证程序的稳定性。
-
之前使用PreNew+CMS的原因: 因为项目的业务逻辑简单,因此在运行中很少有对象能进入老年代中,所以在堆内存中基本上都是存活在年轻的的对象,推测是无法发挥出G1的优势。因此之前的想法是将堆内存减少,同时利用PreNew的并行收集来尽量缩减MinoGC的时间。但是实际情况发现频繁的MinoGC反而导致吞吐量下降了
-
优化后的GC日志
//线上案例 zhenai-mobile-api
#!/bin/bash
/usr/bin/java -cp /data/dubbo/zhenai-mobile-api/classes:/data/dubbo/zhenai-advertising-api/libs-zhenai/*:/data/dubbo/zhenai-advertising-api/libs/*
-Xms5734m // 最小堆内存5G+
-Xmx5734m // 最大堆内存5G+
-Xss512k // 线程栈,一个线程分配512k
-XX:MetaspaceSize=512m //元数据区最小512
-XX:MaxMetaspaceSize=512m // 元数据区最大 512
-XX:+UnlockExperimentalVMOptions
-XX:+UseCGroupMemoryLimitForHeap
-XX:+UseG1GC // 使用G1 垃圾收集器
-XX:MaxGCPauseMillis=100 // 设置GC暂停等待时间,单位为毫秒
-XX:+ParallelRefProcEnabled