简述常见三种GC和四种GC回收器

1 常见的三种Garbage Collection
1.1 Minor GC
从年轻代空间(包括Eden和Survivor区域)回收内存被称为Minor GC :

当Eden区域满了,jvm无法为新对象分配内存,会触发Minor GC;
新生代好进行标记和复制操作,就不会存在内存碎片。
年轻代中指向永久代中的引用,在标记阶段就会忽略。
stop-the-world。原因是Eden区中对象认为是垃圾,不会复制到Survivor区或者老年代。如果相反,Eden区
大部分对象不符合GC 条件,那么 Minor GC指定的时间就比较长。每次Minor GC会清理年轻代的内存。
1.2 Major GC 和Full GC
Major GC: 清理老年代
Full GC: 清理整个堆内存,包括年轻代和老年代
但是更多情况下,许多Minor GC 会 触发Major GC ,所以实际情况两者分离是不可能的。这就使得我们关注重点变成,GC是否能并发处理这些GC.

2 四种垃圾回收器
2.1 串行Serial Collector 单线程回收
它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。

从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法。清空整个Heap中的垃圾对象,清除元数据去已经被卸载的类信息,并进行压缩。
新生代的复制算法。Serial GC 的对应 JVM 参数是:-XX:+UseSerialGC
2.2 并行回收器(Paraller Collector ) :
又称throughput collector 。是jvm默认的回收器。Serial的升级版本,多线程进行GC。常见应用场景是配合老年代CMS GC工作参数如下:

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
1
根据Minor GC 和Full GC 的划分为

ParNewGC : 通过-XX:+UseParNewGC参数来指定,多线程并行回收的Serial GC版本。
ParallerGC:Server下默认的GC方式,通过-XX:+UseParallelGC参数指定,并发回收线程数可以用-XX:ParallelGCThreads指定。清除Heap中的部分垃圾对象,并进行部分空间压缩。
ParallerOldGC:可以通过-XX:UseParallelOldGC参数指定。并发回收线程数用 -XX:ParallelGCThreads来指定。与ParallerGC的不同之处在于Full GC上,前者Full GC是将清空整个Heap中的垃圾对象,清除元数据去已经被卸载的类信息,并进行压缩。而后者是清除Heap中的部分垃圾对象,并进行部分空间压缩。
2.3 并发标记扫描收回器(CMS Collector) :
管理新生代方式和Paralel和Serial GC相同。而是在老年代中并发处理。尽量减少停顿时间。 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

2.3.1 基础
CMS(Concurrent Mark Sweoop):并发标记清除回收器。管理新生代和Paraller和Serial回收器相同。而老年代尽可能并发执行,每个垃圾回收周期都有两次停顿。
CMS 的目的就是为了消除Paraller和Serial回收器在Full GC 停顿周期时间长的问题。
2.3.2 CMS 的整个过程
初始标记(STW):通过一系列GCRoot标记直接可达的老年代对象;新生代引用老年代对象。(此过程在jdk8以前是单线程。8及以后就是多线程,通过CMSParallelInitialMarkEnabled可调),也就是说在此过程中新生代和老年代都会被扫描。
并发标记;经过上一次标记,开始tracing过程,标记所有可达对象。此过程是应用线程也在运行。
由于应用在运行,这样会出现有些对象新、老代之间位置发生改变,对象引用也发生变换,那么怎么确定这些发生变化的对象呢?这些的变化都影响到老年代对象所在card被标记为dirty,为后面的重新标记做准备。
并发预清理
标记老年代存活对象。老年代对象有可能是被并发阶段修改过的对象,所以说,也要扫描新生代。这样老年代和新生代都需要扫描,这个扫描优化是怎么处理的呢?应该有一个种,快速识别存活对象的一种机制。
对于新生代来说:
如果重新在来一个MinorGC 就好
Eden的使用空间大于“CMSScheduleRemarkEdenSizeThreshold”,这个参数的默认值是2M;Eden的使用率大于等于“CMSScheduleRemarkEdenPenetration”,这个参数的默认值是50%。
也就是说预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段。
如果在可中断的时间内没有发生minor GC 怎么办呢?CMS有一个参数CMSMaxAbortablePrecleanTime默认是5s,也就是说5秒后,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,自动进入remark;
那如果五秒内没有执行Minor GC,这个时候新生代还有好多活着对象,这就会使得STW变长。所以提供在重新标记之前强制Minor GC ,参数是CMSScavengeBeforeRemark。
对于老年代来说:
年代的机制就是CARD TABLE的东西(类似与一个数组);他将老年代划分成512byte的块。每一个card Table 对应一个块,如果对象引用发生变化就会变成dirty card;并发预清理阶段就会重新扫描该块,将该对象引用的对象标识为可达;
重新标记 (STW) 重新扫描堆中对象。扫描目的新生代的对象+GC Root+ 前面标记的dirty的card对应的块。多线程来处理这个过程。
E:并发清除。用户线程激活,同时将没有标记存活对象标记为不可达;清理不可用不可达垃圾。在此过程中,用户线程依旧在运行,在此期间产生的新垃圾,在本次GC中没有办法清除。所以这些本次没办法清除的称之为:浮动垃圾。
由于次清除的特性(CMS线程和用户线程都在运行):所以老年代的使用率没办法用到100%使用,CMS中有CMSInitiatingOccupancyFraction(默认92%)参数设置老年代空间使用百分比,达到百分比就进行垃圾回收。
并发重置:CMS 内部重置回收器,准备下一次
2.3.3 CMS的特性
优点:
低延迟收集器,几乎没有停顿时间。只有在出示标记和并发标记的时候出短暂停顿。
缺点
CMS中并发意味着多线程强占CPU的资源。
CMS默认回收线程的公式:(CUP个数+3)/4 .这就意味着如果用户cup个数比较少的,CMS的CPU占用率就很高。显然这种情况以及用硬件打败,现在的机器都是多核处理。
CMS收集老年代会出现内存碎片化现象
不会对内存进行任何的压缩和整理,过多的碎片化内存会出现实际内存不足的情况,所以会出现Full GC的情况CMS 提供两个参数来完成Full GC
① UseCMSCompactAtFullCollection ,在进行Full GC 的过程中进行内存碎片的整理;
② CMSFullGCsBeforeCompaction,每隔多少次不压缩的Full GC ,执行一次压缩Full GC
出现浮动垃圾:在并发清除过程中,用户进程依然在运行,此时产生的垃圾是在本次清除过程中没办法清除。这部分垃圾被称为浮动垃圾。
2.3.5 关于永久代(元数据区的扩容)的垃圾回收
CMS默认情况是不会对永久代进行垃圾处理的。但是可以通过CMSPermGenSweepingEnabled参数来配置永久代的垃圾回收。开启后就会有一组后台线程针对永久代做收集(与触发老年代垃圾回收机制的指标是独立的)。

2.3.6 CMS的并发收集周期的触发问题
CMS的触发有两个条件:

阈值检查机制:由于并发清除过程会产生浮动垃圾。所以老年代的使用率没有办法达到100%。只能到达某一个阈值以后(jdk1.8默认值92%,1.6之后是92%,1.5默认是68%),或者通过CMSInitiatingOccupancyFraction和UseCMSInitiatingOccupancyOnly 两个参数来调节;过小会造成GC频繁;过大,导致并发模式失败。
动态检查机制:JVM会根据最近的回收历史,估算下一次老年代被耗尽的时间,快到这个时间了就启动一个并发周期。 可以用UseCMSInitiatingOccupancyOnly来将这个特性关闭。
2.4 G1 垃圾回收器
G1 (Garbage first ):是JDK7的新特性。jdk7以上都可以自主设置JVM GC类型。G1会将堆内存划分成相互独立的区块(默认1024),每一块都可能是不连续的 O(old区),Y(young区)区块(相对于CMS中O,Y区块是连续的)。G1会第一时间处理垃圾最多的区块。这个是garbage First的原因之一。
新生代的回收算法主要是复制算法,但是新生代的分区机制主要是为了便于调整区块大小的。

2.4.1 G1和CMS 相比突出的优势
G1在压缩空间方面有优势

G1通过将内存空间分成区域(Region)的方式避免内存碎片问题

Eden、Survivor、Old区域不在固定,在内存使用率上更加灵活

G1可以设计预期停顿时间(Pause Time)来控制垃圾回收时间避免引用雪崩的现象。

G1在回收内存后会马上同时做合并空闲内存的工作、CMS默认情况是STW的时候合并内存。

G1会在Young GC中使用、而CMS在O区中使用

2.4.2 关键词解释
Regin
G1默认把堆内存分为1024个分区,后续垃圾收集的单位都是以Region为单位的。Region是实现G1算法的基础,每个Region的大小相等,通过-XX:G1HeapRegionSize参数可以设置Region的大小(取值范围是2~32,且为2的指数)

在上图中出现一些Region表明了H,代表Humongous,这些区域表示Region存储的是巨大对象(humongous Object ,H_obj) 大小大于等于Region一半的对象。
H_Obj有一下几个特征

H_obj最直接分配到old gen,防止了反复拷贝移动;

在分配内存之前检查是否超过initiating heap occupancy percent(启动堆占用比例)和the marking threshold(标记阈值),如果超,会启动global concurrent marking,为的是提早回收,防止evacuation failures 和full GC.

为了减少连续额H_objs对象对GC的影响,需要把大对象变成普通的对象, 建议增大Region size。

SATB (Snapchat-At-The_Beginning)GC开始时活对象的一个快照
它是通过Root Tracing得到的,作用是维持并发GC的正确性,也是G1并发的基础。可以理解成GC开始之前堆内存的对象做一次快照。标记活着的对象。形成对象图。

如何维持正确性呢?根据三色标记算法(对象的三种存在状态)
白色:对象没有标记到,标记阶段结束后,会当做垃圾回收。
灰色:对象被标记了,但是它的field还没有标记或还没有标记完。
黑色:对象被标记了,且它的所有field也被标记完了。
并发标记的情况下,Mutator和Garbage Collector 线程同时修改对象。会出现白对象漏标的情况:
Mutator赋予一个黑对象该白对象的引用 :白类 白对象 = 黑对象;
在并发标记阶段,如果该白对象是new 出来的,并没有灰对象持有。 Region中有连个top-at-mark-start(TAMS)指针,分别是prevTAMS和nextTAMS.在TAMS以上的对象是新分配的,这是一种隐式标记,通过这种的方式找到了再GC过程中新分配的对象,并认为是活对象。
Mutator删除了所有从灰对象到该白对象的直接或者间接引用
如果灰对象到白对象的直接引用或间接引用被替换或者删除了,那白对象就会被漏标。从而导致被回收掉,这是非常严重的错误。为了防止这种现象的发生。G1给出了利用write barrier将就引用记录下来,以防止被清除(对象引用被替换是就会发生write barrier)。
副作用
如果被替换的白对象就是要被收集的垃圾,那这次标记就会让它躲过这次GC,这就是Float Garbage(浮动垃圾)。因此SATB的做法精度比较低,所以造成Float Garbage比较多。
Rset(Remembered Set)
简介
辅助GC过程的一种结构,典型的空间获取时间的工具。每个Region都有一个Rset记录的是其他Region中的对象引用本Region对象的关系,属于points-into(谁引用了我的对象)(与之类似的有Card Table),而Card Table则是一种point-out(我引用了谁的对象)的结构, 每一个Card覆盖一定范围的Heap(一般是512Bytes)。


实现过程
G1的Rest是在Card Table的基础上实现的:每个Region会记录下别的Region指向自己的指针,并标记 这些指针的分别在那些Card的范围内。
这个Rset其实是一个Hash Table ,key是别的Region的起始地址,Value是一个Card Table 的index的集合。


上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。 而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护,操作伪代码如下:

void oop_field_store(oop* field, oop new_value) {
    pre_write_barrier(field);             // pre-write barrier: for maintaining SATB invariant
    *field = new_value;                   // the actual store
    post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference
 }
1
2
3
4
5
post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。

RSet究竟是怎么辅助GC的呢?
在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。
而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量

类似结构
还有一种数据结构是辅助GC:Collection Set(Cset) Cset主要记录GC要收集的Region集合,集合中的Region是任意代的。 在GC的时候,对old->young和old->old的跨代对象引用,只要扫描对应的Cset中的RSet即可。

停顿预测模型(Pause Prediction Model)
G1使用暂停预测模型来满足用户定义的暂停时间目标,并根据指定的暂停时间目标选择要收集的区域数量。
G1 是一个相应时间优先的GC算法,它与CMS最大的不同是:用户可以设定整个GC过程的期望停顿时间。参数-XX:XX:MaxGCPauseMillis指定一个G1收集过程目标期望停顿时间,默认值200ms。G1根据整个模型统计计算出的历史数据来预测本次收集需要Region数量,从而满足用户设定 的目标停顿时间。停顿预测模型是以衰减标准偏差为理论基础实现的。

2.4.3 GC过程
2.4.3.1 G1 GC 模式
G1 提供了两种GC模式,Young GC和Mixed GC,这两种都是完全的STW的。

Young GC:选定年轻代里的Region,通过控制年轻代的Region个数,即年轻代内存的发小,来控制Young GC的时间开销。
Mixed GC:选定年轻代里面的Region,外加根据global concurrent marking统计出收集收益高的若干老年代Region。
在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

由上面可知,Mixed GC不是Full GC ,它只能部分回收老年代的Region,如果Mixed GC实在无法跟上程序分配内存速度,导致老年代填满无法继续进行Mixed GC ,那就会使用Serial Old GC(Full GC)来收集整个GC Heap。而G1不提供Full GC。
G1的运行过程是这样的,会在Young GC和Mix GC之间不断的切换运行,同时定期的做全局并发标记,在实在赶不上回收速度的情况下使用Serial old GC。
初始标记是搭在YoungGC上执行的,在进行全局并发标记的时候不会做Mix GC,在做Mix GC的时候也不会启动初始标记阶段。
当MixGC赶不上对象产生的速度的时候就退化成Serial old GC,这一点是需要重点调优的地方。

2.4.3.2 过程简述
G1垃圾回收分为两个阶段:全局并发标记和拷贝存活对象阶段。

全局并发标记(global concurrent marking)
前面多次提到global concurrent marking(全局并发标记),它的执行过程有点类似CMS,但是不同的是,在G1 GC中,它主要是为了Mixed GC提供服务的,并不是一次GC过程的一个必须环节。

初始标记(initial mark ,STW):标记了从GC Root开始直接可达的对象。
并发标记(Concurrent Marking):GC Root 开始对heap的对象标记,标记线程与应用程序线程并发执行,并收集各个Region的存活对象信息。同时还会扫描SATB write barrier所有记录下的引用。
最终标记(Remark ,STW)标记那些在并发标记阶段发生变化的对象,将被回收。
清除垃圾(Cleanup):清除空Region(没有存活对象的),region加入到空闲列表中。这个阶段并不会实际上去做垃圾的收集,只是去根据停顿模型来预测出CSet,等待evacuation阶段来回收。
拷贝存活对象阶段(Evacuation)
Evacuation阶段是全暂停的。该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理。
Evacuation阶段从第一阶段选出来的Region中筛选出任意多个Region作为垃圾收集的目标,这些要收集的Region叫CSet,通过RSet实现。筛选出CSet之后,G1将并行的将这些Region里的存活对象拷贝到其他Region中,这点类似于ParalledScavenge的拷贝过程,整个过程是完全暂停的。关于停顿时间的控制,就是通过选择CSet的数量来达到控制时间长短的目标。

G1最佳实践
在使用G1垃圾收集器的时候遵循以下实践可以少走不少弯路:

不断调优暂停时间指标
通过XX:MaxGCPauseMillis=x可以设置启动应用程序暂停的时间,G1在运行的时候会根据这个参数选择CSet来满足响应时间的设置。
一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。
不要设置新生代和老年代的大小
G1收集器在运行的时候会调整新生代和老年代的大小。通过改变代的大小来调整对象晋升的速度以及晋升年龄,从而达到我们为收集器设置的暂停时间目标。
设置了新生代大小相当于放弃了G1为我们做的自动调优。我们需要做的只是设置整个堆内存的大小,剩下的交给G1自己去分配各个代的大小。
关注Evacuation Failure
Evacuation Failure类似于CMS里面的晋升失败,堆空间的垃圾太多导致无法完成Region之间的拷贝,于是不得不退化成Full GC来做一次全局范围内的垃圾收集。
参考链接:
https://tech.meituan.com/2016/09/23/g1.html
https://www.cnblogs.com/yunxitalk/p/8987318.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值