by shihang.mai
0. 概念
并发: 工作线程与GC线程同时执行
并行: 多个GC线程同时执行
1. 常见的垃圾回收器组合
常见垃圾回收器组合
Y区 | O区 | 特点 | 适用内存 |
---|---|---|---|
Serial(copying) | Serial Old(mark-compact) | 单线程回收 | 几十兆 |
Parallel Scavenge(PS-copying) | Parallel Old(PO-mark-compact) | 多线程并行回收,默认回收组合 | 上百兆 - 几个G |
PartNew(copying) | CMS(mark-sweep) | PartNew是PS的一个变种,为了配合CMS使用。默认线程数=CPU核心数 | 20G |
G1 - 上百G
ZGC - 4T - 16T
2. 各垃圾回收器详解
2.1 Serial+Serial Old
使用场景:在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合
使用方法:-XX:+UseSerialGC = Serial New (DefNew) + Serial Old
工作流程:stw,清理
java -XX:+PrintFlagsFinal -version //查看所有的参数的最终值
-Xmn //设置年轻代大小
-Xms40M -Xmx60M //设置堆最小最大,一般都要设置成一样,因为防止浪费在内存回弹
-Xss //设置栈大小
-XX:NewRatio = 4//配置新生代与老年代在堆结构的占比,老年代是新生代的4倍,默认是2
-XX:SurvivorRatio =8//设置新生代中Eden和S0/S1空间的比例,eden:s1:s2 = 8:1:1
-XX:MaxTenuringThreshold =15//设置对象存活多少次才能进入老年代
-XX:PrintPromotionFailure =false//是否设置空间分配担保
2.2 PS+PO
使用场景: 在并发能力比较强的 CPU 上,并且追求吞吐量
使用方法: -XX:+UseParallelGC = -XX:+UseParallelOldGC = PS+PO
工作流程:stw,清理
-XX:MaxGCPauseMillis //最大垃圾回收停顿时间
-XX:GCTimeRatio = 99 //设置吞吐量大小参数,最大GC时间占总时间公式1/(1+99)=1%
-XX:+UseAdaptiveSizePolicy//开启jvm根据当前运行情况动态去调整最大吞吐量和停顿时间等参数,自适应调节策略
2.3 ParNew+CMS
使用场景: 在并发能力比较强的 CPU 上,并且追求响应时间
使用方法:-XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
工作流程:
- 工作线程执行
- 初始标记。会进行stw,标记GC Root
- 并发标记:stw状态恢复。并发标记环节运用了三色标记+Incremental update算法
- 重新标记。会进行stw,重新标记是因为并发标记过程,会产生新的垃圾。
- 并发清理。但是并发清理过程中,工作线程也会产生垃圾,这些垃圾叫做浮动垃圾,这些垃圾等待下次CMS才能回收
注意点:
因为它用的是mark-sweep算法,会造成内存碎片化。为了解决堆空间浪费问题,把一些未分配的空间汇总成一个列表,当 JVM 分配对象空间的时候,会搜索这个列表找到足够大的空间来存放住这个对象。
jdk5,默认当老年代使用68%的时候(jdk6后92%),CMS就开始行动了。
CMS在以下两种情况会触发单线程回收
-
并发模式失败(concurrent mode failure)
当 CMS 在执行回收时,新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时
-
晋升失败(promotion failed)
当新生代发生垃圾回收,老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败
2.4 G1
2.4.1 G1的概念
使用场景: 配备多核处理器及大容量内存的机器(上百G)。追求响应时间
,大于等于jdk8以上才有G1,并且在jdk10及之前都是单线程回收
使用方法: -XX:+UseG1GC
G1 GC 首先将堆分为大小相等的 Region, 而g1可以为Region 动态地分配给
- Eden
- Survivor
- 老年代
- 大对象空间(当对象大小超过 Region 的一半,则认为是巨型对象,直接被分配到老年代的巨型对象区)
- 空闲区间
G1 把堆内存划分成一个个 Region 的意义在于:
每次 GC 不必都去处理整个堆空间,而是每次只处理一部分 Region,实现大容量内存的 GC。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收获得的空间大小以及回收所需时间的经验值),在后台维护一个线性列表,每次根据允许的收集时间,优先回收价值最大的Region。
Rset、Cset、CardTable、Region关系:
- Cset: Collection Set,它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可
- Rset: Rememberd Set,每个Region都有一个RSet,RSet记录了谁引用了当前Region,它其实是一个hash table(key: 别的Region的起始地址 value:Card Table的Index集合)
- CardTable: 每个Card 覆盖一定范围的Heap(一般为512Bytes)
如图所示
- RegionA、B、C分别切分为4个card
- card1中的对象引用了card4、card6中的对象
- card2中的对象引用了card6中的对象
- card10中的对象引用了card6中的对象
那么出现右边的图示,首先明确Rset是属于每个Region的;CardTable是属于每个Card的,并且它是一个bitmap
- card 1的bitmap,如右上方所示,第5、7位置为1,表示card1引用了card4、card6
- card 10的bitmap,如右下方所示,第7位置为1,表示表示card 10引用了card6
- RegionB的Rset,如右中方所示,记录的是其他Region引用当前Region的信息,记录了两组数据
- key: RegionA的起始地址 val: card1、card2
- key: RegionC的起始地址 val: card10
正因为有Rset的存在,大大加快了GC的速度
- 在做YGC的时候,只需要选定young region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。
- mixed gc的时候,old generation中记录了old->old,young->old的RSet,old->young的引用由扫描全部young region的Rest得到,这样也不用扫描全部old region
Rest的维护:
对于Rest的维护并不是修改完引用对象时,立刻去修改,而是有一种异步的思想,利用write barrier将【跨Region的引用更新】记录缓存日志中,当缓存日志满了,write barrier停止,由另外一条线程(Concurrent refinement threads)处理这些缓冲区日志,更新到Rest中
2.4.2 G1 GC模式
G1 GC的工作模式有2个, Young GC 和 Mixed GC
Young GC
- 发生时机: 年轻代Region用尽
- 范围: 选定所有年轻代里的Region。。
- 控制gc时间的措施: 计算下次 Young GC 所需的 Eden 区和 Survivor 区的空间,动态调整新生代所占 Region 个数来控制 Young GC 开销。即通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销
Mixed GC
- 发生时机: 由参数
G1HeapWastePercent
控制,在标记回收价值结束之后,我们可以知道old region中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC - 范围: 选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region
- 控制gc时间的措施: 在用户指定的开销目标范围内尽可能选择收益高的老年代Region
- 工作流程:
- 标记阶段
1.1 初始标记阶段
会进行STW,标记一下GC Roots
1.2 并发标记阶段
恢复STW,从GC Roots开始对堆中对象进行可达性分析,找出存活对象。使用的是三色标记+SATB算法。这一期间的变化记录在Remembered Set Log 里
1.3 最终标记/再标记阶段
进行STW,重新标记那些在并发标记阶段发生变化的对象。并把Remembered Set Log里的信息合并到Remembered Set里 - 清理阶段
该阶段延续上面的STW,清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。 - 复制阶段
该阶段延续上面的STW,使用Mark-compact
做复制压缩,回收价值大的Region,并移动对象(分配新内存和复制对象)。
- 标记阶段
Mixed GC不是full GC
,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap,jdk10前是单线程的。
2.4.3 计算回收价值
- 就是上文提到的global concurrent marking。它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。
- global concurrent marking的执行过程分为四个步骤:
- 初始标记。它标记了从GC Root开始直接可达的对象。
- 并发标记。这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
- 最终标记。标记那些在并发标记阶段发生变化的对象,将被回收。
- 清除垃圾。清除空Region,加入到free list。
- 第一阶段初始标记是共用了Young GC的暂停,所以可以说global concurrent marking是伴随Young GC而发生的。第四阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW
3. ZGC
ZGC适用于大内存低延迟服务的内存管理和回收
,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。为什么转移阶段不能和标记阶段一样并发执行呢?主要是G1未能解决转移过程中准确定位对象地址的问题(当移动对象的过程中,工作线程访问对象,对象可能已经被移动)
3.1 基础概念
- jdk11开始推出的垃圾回收器,它有几个目标
- 停顿时间不超过10ms;
- 停顿时间不会随着堆的大小,或者活跃对象的大小而增加;
- 支持8MB~4TB级别的堆(未来支持16TB)
只支持64位。
各个阶段基本全并发
3.2 回收过程
如图所示,只有3个阶段是STW的,分别是初始标记,再标记,初始转移
,其他阶段全部并发。其中,初始标记和初始转移分别都只需要扫描所有GC Roots,再标记阶段STW时间很短,最多1ms,超过1ms则再次进入并发标记阶段
即,ZGC几乎所有暂停都只依赖于GC Roots集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与ZGC对比,G1的转移阶段完全STW的,且停顿时间随存活对象的大小增加而增加
3.3 核心原理
颜色指针Colour pointer+读屏障
,将"停顿"粒度细化到每个对象上,提升gc效率
并发转移要解决的问题:GC线程在转移对象的过程中,工作线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误
解决方式: 工作线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样工作线程始终访问的都是对象的新地址
怎么做到发现对象被移动呢?
,那就是颜色指针了。着色指针是一种将信息存储在指针中的技术。
- 对象状态存储在引用指针上
- 在业务线程访问对象时候,如果发现被着色(利用指针访问对象,很容易发现),那么增加读屏障,等待mark-compact修改完地址后,返回有效地址再撤除读屏障
4. 三色标记算法
4.1 颜色
看下图例,分为黑、灰、白
4.2 并发标记环节漏标
在并发标记环节,由原本A状态变为B状态后,因为A已经是黑色,在最终标记环节并不会重新扫描A去做标记D,所以D漏标了,这样会导致D被回收显然不对
4.3 解决漏标
跟踪A->D的增加。算法名称:Incremental update。将A重新变为灰色,remark阶段重新标记,那么保证了D不会漏标
跟踪B->D的消失。算法名称:SATB。将B->D的引用重新放到GC的栈,栈里面存放都是是灰色->白色的引用,正因我把消失的引用重新放进去,remark阶段扫描这个栈去标记,那么保证了D不会漏标
G1使用SATB 而不用Incremental update?
因为用Incremental update的话,A的引用要全部扫描一篇,而用SATB的话,只需将改变的扫描,效率高.
考虑以下场景:
1. 如果B->D消失,把B->D的引用放入到GC栈,但是没有A->D
2. 在Remark时,拿到这个引用,D里面因为有RSet的存在,并不需要扫描整个堆有什么对象指向D,直接查找D里面的RSet即可,直接就可以回收D
5. G1与CMS的异同
- 同:
都存在并发标记过程 - 异
- 分代模型: CMS物理分代,G1逻辑分代Region;
- 浮动垃圾: CMS有浮动垃圾,G1无;
- 有无压缩算法,有无碎片化: CMS用标记-清除法(可通过JVM参数设置CMS压缩),G1用标记-整理法;
- 筛选回收:CMS是老年代一并回收,G1是通过预测垃圾优先级别高的进行回收;
- 数据结构,G1存在RSet,在每一个Region里面,记录了其他Region中的对象对自己的引用,当垃圾回收的时候就不用扫描整个堆就找到谁引用了当前Region中的对象,只需扫描RSet即可(G1高效回收的关键);card table:(年轻代要确定有没存活对象,要在老年代扫描一次有没引用年轻代的对象,这太费劲了)由于做YGC时,需要扫描整个OLD区,效率非常低,所以JVM设计了CardTable, 如果一个OLD区CardTable中有对象指向Y区,就将它设为Dirty,下次扫描时,只需要扫描Dirty Card 在结构上,Card Table用BitMap来实现
- 可预测停顿时间: G1可以预测,CMS不能
参考
https://tech.meituan.com/2016/09/23/g1.html
https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html