php转java重构搭脚手架的时候,因为是从新开始的项目,所以打算直接升jdk版本+spring boot3.0+。查了下jdk17自带的zgc是保障低延迟的,并且为了保障低延迟,需要的额外空间开销比较大,出于以下角度考虑,还是选择了G1
- 我们的堆往往没那么大,所以内存比较宝贵,尽可能减少额外的内存额外开销
- 我们对吞吐量的要求明显高于低延迟,单接口百ms级别都是可以接受的,此时zgc的毫秒级别延迟在整个接口百ms的基准下影响不大,为了这点延迟去降低实际可用内存得不偿失
- G1 GC近十来年了,ZGC还较为新,使用G1 GC相关的问题以及调优可以参考的更多
其实CMS更适合,不过用新不用旧,并且G1在大多数情况下它的GC逻辑也是接近CMS的
如何学习
个人一开始尝试去理解一些复杂概念,但是这些复杂概念只是通过论述很难彻底理解,所以转为了解这些结构的功能而非实现,了解GC的过程而非细节,以便于实际工作调优或者排查问题。
毕竟除非需要去修改GC的源码,应该是不需要对里面使用到的一些数据结构和方法进行详细的了解(但是大部分博客文章中往往反而会讲,就会讲的你很晕)。
这篇笔记记录给自己不定时翻出来看看回忆下,不会带一些需要大量篇幅才可以解释清楚的细节。
G1 GC的GC模式
内存分布
G1 中,将内存划分为多个大小相同的region,其中可以简单的认为分为新生代region和老年代region
与CMS相同:
- 新生代在N次GC后未被回收,进入老年代
- 大对象直接进老年代
GC收集模式
G1 GC的GC模式包含以下几种收集模式(内容来源于R大):
- young GC(或者叫minor GC):只收集young gen里的所有region,也就是eden和survivor。控制young GC开销的手段是动态改变young region的个数;
- mixed GC:收集young gen里的所有region,外加若干选定的old gen region。控制mixed GC开销的手段是选多少个、哪几个old gen region。
- 其实没有3了。G1 GC的控制范围内没有full GC。如果mixed GC无法跟上mutator分配的速度,导致没有足够的空region来完成mixed GC,那么就会使用serial old GC( mark-compact)来对整堆收集一次。
所以G1 GC的GC,一般都在几个过程里:
- 要么在young gc,只回收新生代,可以类比和CMS一起用的young gc,用的是也是标记复制
- 要么在mixed GC,通过一次young GC来先做标记,然后再并发标记,并发标记后根据一个回收的耗时评估+你配置的预期停顿时间,来决定回收那些老年代old region。
- 要么在full gc,新生代和老年代都无法分配内存了,直接进入最原始的STW然后遍历整个堆做收集
G1 的GC细节(不那么细的)
G1 性能方面的优化
解决问题:新生代GC时,老年代对新生代的引用问题
概念1:卡表card table
可以先看下https://www.cnblogs.com/binyue/p/17281785.html
在新生代GC时,可能存在老年代对新生代的引用,此时想要回收新生代的对象,还需要扫描整个老年代吗?那岂不是等于full gc(扫描整个堆)了?卡表使用额外的空间减少扫描的消耗。
划重点:卡表是一个数据结构,用于记录老年代对象对新生代的引用,此时回收新生代时,只需要扫描那些卡表中有的老年代对象即可(不需要care卡表的实现细节
概念2:Remember Set
Remember Set是G1中用于解决回收时老年代引用新生代的问题的,因为G1的回收是针对Region的,所以要保证每个Region可以单独被回收,那么每个Region就需要记录自己的卡表
划重点:Remember Set是每个region维护的一个数据结构,里面维护了自己的卡表,在新生代回收时用于减小老年代扫描的规模
G1 回收正确性
解决问题:解决并发标记的正确性
因为标记过程与用户线程执行是并发的,所以会存在你标记后但是被修改了的情况,那么就会漏标或者多标
概念1:黑白灰标记
这里有一个模型来简化问题,即黑白灰标记,使用黑白灰来标记对象
- 黑色 - 该对象已经标记,且该对象的引用字段也都已经处理完或已经加到任务队列中
- 灰色 - 该对象已经标记,但是对象的引用字段还没有处理完
- 白色 - 该对象还没有标记
概念2:satb以及浮动垃圾
satb是一个解决并发标记问题的方案。在一开始,认为所有对象都是存活的。然后在标记过程中,使用一个数据结构,通过代码中在改变引用的前后加上指令,实现一个切面一样的效果,以此来在这个对象的引用改变的时候,记录那些变更了引用的对象到satb里去。
在实际清理前,只需要扫描satb里的对象就可以了。
satb会产生浮动垃圾(该回收的没标记,下次才能回收),但是不会漏标(不会因为并发标记的错误而导致实际内存泄露,即保证了正确性)。
概念3:新产生的内存,TopAtMarkStart,TAMS指针
在并发标记执行后,实际回收执行前。新产生的内存也应该是存活的,所以需要避免回收这部分,在region内定义一个指针,记录并发标记时的地址,大于该指针的内存全是并发标记后新产生的。
G1 回收保证吞吐
G1进行并发标记、最终标记、清理这三个阶段后,实际并没有回收内存。这里的清理阶段只是清理一些标记状态,并且把完全没有活对象的region整体回收到可分配的region列表里。
实际清理后,有一个完全可以独立执行的evacuation阶段,这个阶段根据标记的结果,分析每个老年代的region可回收信息,然后经过一堆统计学算法和你配置的信息的评估,决定回收哪些region。然后活对象拷贝到空region里去,回收原本的region的空间。
参考: