JVM垃圾回收需要考虑的两个点:停顿时间短、高吞吐量。
不同的要求,不同的收集模式(收集过程)。
如何保证垃圾收集准确,速率高,线程数多少,占用的内存情况都需要考虑。
为什么需要stw(stop the world)?
jvm GC优化:
- 硬件配置
- 业务场景(吞吐量,延迟,内存使用)
串行收集:
串行垃圾收集器有:
Serial收集器\Serial Old收集器:
优势:单线程也意味着复杂度更低、占用内存更少
缺点:如果是多核服务器,无法利用多核的优势。
使用场景:适合堆内存不高、单核甚至双核CPU的场合
使用算法:Serial(复制清除算法)、SerialOld(标记-整理算法)
收集区域:Serial-年轻代、SerialOld-老年代
调优参数:
并行收集:
有的资料说这种模型是并行,这里我并不觉的是并行,我觉得还是串行,这里只是在做GC的时候,用多个线程去处理。解决了SerialGC单线程无法利用多核cpu优势的问题。
Parallel Scavenge 收集器:
优势:多线程
缺点:
使用场景:
使用算法:复制-清除算法
收集区域:年轻代
调优参数:-
XX:MaxGCPauseMills
和-XX:GCTimeRatio
,调整新生代空间大小,来降低GC触发的频率
并发收集:
并发收集也就是用户线程后GC线程同时工作,这也就是说线程数会被GC工作占用一部分,所以说这里吞吐量可能不是你业务主要考虑的。
工作过程:初始标记(initial mark)->并发标记(concurrent mark)-> 预清理->可中断预处理->重新标记->回收
详细查阅:
jvm-cms垃圾收集器
CMS 收集器:
优势:
缺点:
- 用户线程和GC线程并发执行,这里GC线程会占用内存,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制。这时候会降级到串行老年代收集器,将会以STW的方式进行一次GC,从而造成较大停顿时间;
- 标记清除算法->空间碎片,老年代空间会被耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。
使用场景:
- 适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器
- 延时要求高,吞吐量要求低
使用算法:标记-清除算法
收集区域:老年代
调优参数:-XX:CMSFullGCsBeForeCompaction
Garbage First (G1)
他对分代的模型不一样,逻辑上将内存分为很多个小的区域(region)。
Region
将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
对比:
- 之前我们GC的区域是整个年轻代或者老年代,现在G1收集的是其中某个或者某几个region
- 收集的时间不一样,之前的会在内存将要耗尽的时候,或者分配不足的时候回收,G1会去检测老年代是否需要回收,主动进行垃圾的回收(应该是这样啊)
优势:
缺点:
使用场景:
使用算法:
收集区域:年轻代\老年代
调优参数:
G1收集器什么时候发生GC
我们知道G1和其他收集器不同,是将内存划分成多个不同的region区(包括eden、survivor、Humongous、old 、free),之前的会根据新生代和老年代的大小限制来触发gc,现在没有这些了。G1是根据什么来触发GC的呢?
两个配置:
配置最大的eden region 百分比:-XXG1MaxNewSizePercent
设置的stopTheWorld时间:
这里不是非要达到这个百分比,才会触发GC。我们知道这里eden region会不断的增加(这里g1与其他收集器不同,开始分配的eden region比较少,不够的时候,会继续分配 eden region),G1会不断记录region中需要回收的card,并计算回收这些card(region)所需要的时间,如果这个时间和我们设置的(stw)时间差不多,则触发一次GC。
当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC
(保证新生代GC的时候导致的STW在你预设范围内)
ygc触发的契机就是在Eden分区数量达到上限时。一次ygc会回收所有的Eden和survivor区
TLAB(Thread Local Allocation Buffer)本地线程缓冲区: 应该是给用户线程分配的内存,然后新生成的对象,也就是在这块内存中,也就是eden region会在这个部分。
PLAB(Promotion Local Allocation Buffer) 晋升本地分配缓冲区
在ygc中,对象会将全部Eden区存货的对象转移(复制)到S区分区。也会存在S区对象晋升(Promotion)到老年代。这个决定晋升的阀值可以通过MaxTenuringThreshold设定。晋升的过程,无论是晋升到S还是O区,都是在GC线程的PLAB中进行。每个GC线程都有一个PLAB。
Remember Set (RSet):
每个region会有一个RSet,记录老年代中引用eden region中对象(每个card)的指针。
ygc的时候,只要扫描RSet中的其他old区对象对于本young区的引用,不需要扫描所有old区
mixed gc时,扫描Old区的RSet中,其他old区对于本old分区的引用,一样不用扫描所有的old区。提高了GC效率。因为每次GC都会扫描所有young区对象,所以RSet只有在扫描old引用young,old引用old时会被使用。
为了防止RSet溢出,对于一些比较“Hot”的RSet会通过存储粒度级别来控制。RSet有三种粒度,对于“Hot”的RSet在存储时,根据细粒度的存储阀值,可能会采取粗粒度。
这三种粒度的RSet都是通过PerRegionTable来维护内部数据的
Per Region Table (PRT)
RSet在内部使用Per Region Table(PRT)记录分区的引用情况。由于RSet的记录要占用分区的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区的可用空间。G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:
稀少:直接记录引用对象的卡片索引
细粒度:记录引用对象的分区索引
粗粒度:只记录引用情况,每个分区对应一个比特位
由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆扫描才能找出所有引用,因此扫描速度也是最慢的。
Collection Sets(CSets):
GC收集时,需要或者说被指定的需要回收的region集合。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中
YoungCSets:只容纳年轻代分区
MixedCSets: 候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比
-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。
YoungGC年轻代收集
在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为YoungGC会进行根扫描,所以会stop the world。
YoungGC的回收过程如下:
根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
处理Dirty card,更新RSet.
扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
拷贝扫描出的存活的对象到survivor2/old区
处理引用队列,软引用,弱引用,虚引用(下一篇优化中会再讲一下这三种引用对gc的影响)
MixGC混合收集
MixedGC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能够放到CSet里面,有很多参数可以控制。比如G1HeapWastePercent参数,在一次younggc之后,可以允许的堆垃圾百占比,超过这个值就会触发mixedGC。G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,这个old分区会被放入CSet。源码可以看下gc/g1/collectionSetChooser。
MixedGC一般会发生在一次YoungGC后面,为了提高效率,MixedGC会复用YoungGC的全局的根扫描结果,因为这个Stop the world过程是必须的,整体上来说缩短了暂停时间。
MixGC的回收过程可以理解为YoungGC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象标记,过程如下:
1. 初始标记(InitingMark)。标记GC Roots,会STW,一般会复用YoungGC的暂停时间。如前文所述,初始标记会设置好所有分区的NTAMS值。
2. 根分区扫描(RootRegionScan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前YoungGC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(RootRegion)
3. 并发标记(ConcurrentMark)。会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。
4. 最终标记(Remark)。主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW,可以参考上文的SATB处理。
5. 清除阶段(Clean UP)。上述SATB也提到了,会进行bitmap的swap,以及PTAMS,NTAMS互换。整理堆分区,调整相应的RSet(比如如果其中记录的Card中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。
清除阶段之后,还会对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。
FullGC
G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发FullGC。FullGC使用的是stop the world的单线程的Serial Old模式,所以一旦触发FullGC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full gc的处理的。对于G1 GC的优化,很大的目标就是没有FullGC。
更多可以参考(比较详细):
https://blog.csdn.net/coderlius/article/details/79272773
https://blog.csdn.net/lijingyao8206/article/details/80513383