JVM的那些你想了解的垃圾回收器

JVM调优其实很大程度上跟使用的垃圾回收器也是息息相关的。熟悉并且掌握常见的垃圾回收器会让你的调优之路变得更加的轻松与惬意。这一章会跟大家着重的来讲讲过去以及现在用的比较流行的垃圾回收器。听完这章,你可能对JVM调优的思路会更加的清晰。

在聊垃圾收集器之前,先来聊一笔垃圾收集的算法,你可以这么来理解,如果说垃圾收集算法是回收垃圾的理论,那么垃圾收集器就是这些理论的具体实现。

目前的主流虚拟机(包括Hotspot虚拟机)都采用的分代收集理论,将运行时数据区当中的堆又分为了年轻代跟老年代。因为不同分代所存放的对象实际上是有很大区别的,比如年轻代存放的90%以上的都是朝生夕死的对象,这些对象的存活阶段非常的短,再比如老年代的存在的可能是长期存活着的对象,比如spring容器的注册对象等等。针对不同的分代的特性,需要选择使用合适的垃圾收集算法来进行收集。其中垃圾算法包括标记复制算法、标记清除算法、以及标记整理算法。在下面的篇幅我将先重点讲解这三种算法的具体细节。

一、垃圾收集算法

标记整理算法

标记整理算法跟标记复制算法的部分流程类似,先标记非垃圾对象,然后将非垃圾对象往内存的一侧进行移动,移动完之后就会清理掉非垃圾对象所占据内存区域外的其他内存区域
在这里插入图片描述
缺陷

(1)当需要标记的对象过多,会出现回收效率问题

标记清除算法

在堆中,当通过可达性分析算法发现当前是需要进行回收的对象时,对其进行标记(标记垃圾还是非垃圾跟具体的垃圾回收器有关,一般是标记非垃圾对象,然后统一回收垃圾对象)标记完以后再由垃圾回收器对其进行回收。

在这里插入图片描述

缺陷

(1)如果需要标记的对象太多,整个垃圾回收的所需要的时间就会被拉长。这是一个效率问题。

(2)因为是标记完之后就直接进行清除,这会导致堆中存在内存碎片。会影响到堆中的内存使用率,因为对象是需要连续的内存空间进行存放,太多的内存碎片会把堆内存的连续空间岔开,增加存放对象的难度。

标记复制算法

将一块内存分为同样大小的两块内存,每次只使用一半的内存空间。标记也在那一半内存空间内进行,当标记完之后,垃圾回收器进行回收,回收掉垃圾对象,同时将非垃圾对象移动到另外一半的内存空间当中,并且会对内存进行整理,移动非垃圾对象到同一侧。比如年轻代的surivor区就是采用的此种算法。
在这里插入图片描述
这种垃圾算法的出现使得标记清除算法导致的内存碎片跟效率问题都得到了解决。标记复制算法的效率远远高于标记清除跟标记整理。但是这种算法也有他的缺陷。

缺陷

内存使用率降低了,比如500M的内存空间真正使用的只有250M。

二、垃圾收集器

当对垃圾收集理论有了一定的了解之后,我们剩下要做的就是去熟悉垃圾收集器。针对不同的分代特性选择不同的垃圾回收器进行回收可以在一定程度上优化JVM的垃圾回收效率。但是有一点必须要明白,实际上目前没有任何一种垃圾回收器可以说能完美的适应任何一种业务场景,不同的业务场景需要选择的垃圾回收器也是有不同的。这也是jdk开发人员一直再不断的优化垃圾回收器的原因,可能会再将来会有一种垃圾回收器可以极大程度的去适应不同的业务场景。

在这里插入图片描述

年轻代垃圾回收器包括 Serial,ParNew,Parallel

老年代垃圾回收器包括 CMS,Serial Old,Parallel Old

整堆垃圾回收器 G1

(1)年轻代垃圾回收器

Serial

JVM开启该垃圾回收器参数为

-XX:+UseSerialGC 开启年轻代Serial垃圾回收器 使用的回收算法是复制算法

-XX:+UseSerialOldGC 开启老年代Serial垃圾回收器 使用的回收算法是标记整理算法

在这里插入图片描述
Serial是单线程垃圾收集器,适用于单核CPU的使用场景。在进行垃圾回收时,只有一个GC线程会工作,与此同时,在垃圾回收期间整个JVM会STW,也就是所有用户线程都会停止工作,一直到垃圾收集结束。除此之外该垃圾回收器还是CMS垃圾回收器的备选方案。当CMS在回收垃圾时发生并发失败时,会将使用的垃圾收集器调整成为Serial Old单线程收集器,这一点可以通过GC日志来证明。

优点:适用于单核CPU场景,没有线程交互的开销,在单线程垃圾收集器里简单且高效

缺点:在现在多核处理器的机器上这种垃圾回收器已经被淘汰掉了,单线程的回收相对于其他多线程垃圾回收器要回收更长的时间,且效率更加低下

Parallel

JVM开启该垃圾回收器参数为

-XX:+UseParallelGC 开启年轻代Parallel垃圾回收器 使用的回收算法是复制算法

-XX:+UseParallelOldGC 开启老年代Parallel垃圾回收器 使用的回收算法是标记整理算法

在这里插入图片描述
与Serial垃圾收集器不同的一点是,Parallel垃圾回收器可以支持多线程回收垃圾。在多核的CPU机器当中这无疑发挥了其充分的优势。该垃圾收集器默认的收集线程数跟CPU的核数相同。但是如果你想修改,也提供了参数进行修改

可以通过 -XX:ParallelGCThreads指定收集线程数,但是不推荐修改。

Parallel垃圾收集器更着重关注的点在于回收的吞吐量。

吞吐量可以简单理解为
用户代码运行时间/CPU总消耗时间

但是在这回收期间也会发生STW,暂停掉其他所有用户线程。STW对操作用户来说无疑是一个很不友好的体验,用户可能会觉得一卡一卡的。这也是垃圾回收器着重想要去降低或者说改变的一个点,在后面的CMS跟G1垃圾回收器,这STW上面有很大的改变

优点:适用于多核CPU场景,具有较高的吞吐量

缺点:STW的时间无法控制,频繁发生GC可能会导致用户体验不好

ParNew

-XX:+UseParNewGC 开启年轻代ParNew垃圾回收器 使用的回收算法是复制算法

在这里插入图片描述
ParNew垃圾收集器跟Parallel垃圾收集器很类似,都是serial的多线程版本。唯一有一些不一样的是,这个垃圾收集器可以跟CMS来进行搭配使用,对于4-6G堆内存的JVM来说十分受用。

优点:适用于多核CPU场景,具有较高的吞吐量

缺点:STW的时间无法控制,频繁发生GC可能会导致用户体验不好

(2)老年代垃圾回收器

Serial Old

在(1)年轻代Serial垃圾收集器有一起描述,区别点就在于使用的垃圾回收算法

Parallel Old

在(1)年轻代Parallel 垃圾收集器有一起描述,区别点就在于使用的垃圾回收算法

CMS(Concurrent Mark Sweep)

-XX:+UseConcMarkSweepGC 开启老年代CMS垃圾回收器 使用的回收算法是标记清除算法

CMS垃圾收集器是一款以提高用户体验为由的垃圾回收器。致力于最大程度的减少GC导致的停顿时间。同时它也是HotSpot的第一款真正意义上的并发垃圾收集器。为什么说并发呢?因为他可以使GC线程跟垃圾线程一起工作。
在这里插入图片描述
CMS的垃圾回收过程相对于前面几种垃圾回收器就要显得复杂多了,主要分为五步。分别是,初始标记,并发标记,重新标记,并发清理,并发重置。

初始标记

这里会短暂STW,暂停所有用户线程。然后所有GC线程开始工作,标记所有gc root的直接引用对象。因为只需要标记直接引用而不需要去扫描整个gc root的引用链,所以STW的停顿时间很短。

什么是直接引用对象,这里举个例子。
在这里插入图片描述

并发标记

并发标记这里不会STW,所有的用户线程跟GC线程并发工作,这里会标记所有gc root的整条引用链上的所有对象,这一阶段所花费的时间会比较长。同时,因为没有STW,所以这一阶段可能会出现多标或者漏标的情况,具体的原因会在下面讲解CMS的垃圾回收算法时跟大家重点探究一下。

重新标记

重新标记这里会STW,因为这一步主要是为了纠正在并发标记过程中因为用户线程没有被暂停而导致的多标或者漏标的情况,所以就不会再允许用户线程在这个阶段跟gc线程并发执行。最终目的就是标记出所有不需要被回收的对象

并发清理

这一阶段不会STW,用户线程与gc线程同时执行,清理掉经过三次标记阶段之后未被标记的垃圾对象。
(在这个阶段,新增的还未被标记过的对象会置为黑色,且不会做任何处理)

并发重置

这里就是清理掉本次GC过程中被标记过的对象,去除所有对象的标记,以免干扰下一次进行垃圾回收。

优点:适用于多核CPU场景,STW的时间较短,可以提高用户的操作体验

缺点:
(1)因为GC过程中有并发阶段,并发阶段GC线程会跟用户线程发生资源争抢,这也是提供用户操作体验所做的一种取舍
(2)在并发标记跟并发清理阶段会有浮动的垃圾产生(也就是在并发阶段新生成的垃圾对象,没有经过初始标记,这种浮动垃圾需要等待下一次回收)
(3)采用标记清除算法进行垃圾回收,会有内存碎片的产生。但是可以通过-XX:+UseCMSCompactAtFullCollection来控制CMS每次回收之后进行内存整理,这个参数默认开启。同时可以用-XX:CMSFullGCsBeforeCompaction来控制要经过多少次GC才会触发一次内存整理,这个参数默认值是0,以为着每次GC之后都会进行一次内存整理。
(4)如果在并发阶段产生了大量的新的垃圾,在这一次GC过程还没结束时又要触发下一次GC,那么会停止掉并发,将并发也变成STW阶段,停止掉用户线程,只执行GC线程。同时会将CMS垃圾收集器改成Serial串行垃圾收集器。

三色标记算法

三色标记一共有三种颜色,分别是黑色、灰色、白色。
黑色的对象:表示当前对象的gc root的引用链已经全部被垃圾回收器所扫描并且标记过了

灰色的对象:表示当前对象的gc root的引用链至少有一条或者一条以上已经被扫描并且标记了

白色的对象:表示当前对象没有被gc root所引用,属于需要被回收的垃圾对象

算法执行步骤
(1)最开始所有的对象都默认标记为白色

(2)当触发了老年代GC的时候,CMS垃圾回收器开始执行垃圾回收

(3)垃圾回收的过程初始标记阶段会扫描gc root对象,以及该对象的所有引用对象,如果当前被扫描对象的所有引用都已经被扫描过了,那么标记为黑色,表示当前是安全的对象,如果当前被扫描对象至少有一个或者一个以上的引用对象被标记了,那么标记为灰色,表示当前是安全的对象。如果当前对象都没有指向该对象的引用时,标记为白色,表示是要被清除的垃圾对象。

(4)在并发扫描阶段由于用户线程也可以开始执行了。那么之前已经标记过的三种颜色的对象在这个阶段就有可能重新变成其他颜色。比如已经标记为黑色或者灰色的对象重新标记为白色,这种就算多标,多标没有太大的影响,顶多这种对象成为浮动垃圾,等待下一次垃圾回收。另外一种就是已经标记为白色的对象重新被标记为灰色或者黑色,这种就算漏标,漏标无疑i是一个严重的bug。漏标的对象被清理完那还得了。针对漏标这种可能出现的情况,也给了两种解决方案
(方案A)增量更新
(方案B)原始快照
(5)在重新标记阶段会把之前漏标或多标的对象重新标记,这一次就会确定了哪一些对象是需要被清除的,哪一些对象是不需要被清除的。但是这里又会有一个问题,并发阶段总会有新的垃圾对象生成,这种就是浮动垃圾。这种对象没有经过初始标记,虽然也是垃圾,但是进来就会被标记为黑色,等待下一次垃圾回收的处理。
(6)最后执行并发清理以及并发重置,清理掉垃圾对象,重置非垃圾对象的标记,方便下一次进行垃圾回收时再次重新标记。至此,整个三色标记算法就算执行结束了。

(3)整堆垃圾回收器

G1(Garbage -First)

-XX:+UseG1GC 开启G1垃圾回收器
G1垃圾回收器可以说是垃圾回收器的又一重大变革,他摒弃了物理分代的隔阂,但逻辑上还保留着分代的概念,并且他的GC的STW时间是可以通过参数控制的。来看看下面两张图

在这里插入图片描述

G1把整个堆都分成了一个一个小的region(区域),整个堆默认是分成了2048个region(这是由JVM的源码TARGET_REGION_NUMBER定义的,可以超过该值,但是不建议做),所以如果你给了整个堆分配了4096M,那么分到每一个小的region上面就是2M的大小。当然,这个大小也是可以改变的。可以通过-XX:G1HeapRegionSize来手动指定每个region的size,但是不推荐改变,因为改变这个就会改变region的数目,建议都是用默认值

在这里插入图片描述

逻辑上G1还是保留着分代的概念,还是会有年轻代区域的Eden区跟Survivor区以及老年代的old区。不过这里又多了一个区域,也就是Humongous区域。这个区域是专门用来存放大对象的,当单个对象大于region区域的百分之五十的时候就会被存入Humongous。可能有的同学会奇怪,如果一个格子才2M,那我的对象如果特别大超过了怎么办?如果超过了,就会去找多个连续的Humongous的region来存放这个大对象。

了解完G1的存储结构,我们再来聊聊他在垃圾回收的时候是如何进行的。
对于G1垃圾收集器,主要有三种gc方式

young gc

young gc回收的是年轻代的Eden区域,对于G1来说,年轻代所占的内存大小默认是整个堆内存的5%,通过参数
-XX:G1NewSizePercent可以改变初始的年轻代内存大小。但是并不是说年轻代就一定只有这么多,年轻代的内存空间有一个最大阈值,通过参数-XX:G1MaxNewSizePercent可以指定,默认是堆内存60%
年轻代中Eden与Surivor区域的比例仍然是 8:1:1
假如现在堆内存给了 4096M,那么年轻代占用了204M左右,那么Eden区域就占了163M左右
当Eden区已经用了差不多163M的时候,根据-XX:MaxGCPauseMillis参数来决定是否扩充Eden区,这个参数的意思是GC停顿时间,默认是200ms。当耗时远远低于200ms时,会扩充Eden区域的比例,如果当前耗时已经解决设定停顿时间,那么不会继续扩充,会执行young gc。回收Eden区域的对象,丢入surivor区域。

Mixed gc

在full gc之前的gc。当老年代使用空间达到参数 -XX:InitiatingHeapOccupancyPercent(默认45%)触发
回收所有年轻代以及部分老年代(根据期望停顿的GC时间去回收老年代的垃圾对象)还有大对象区。
使用的复制算法,从一个已使用的region区域把对象挪动到另外一个空闲region区域。当复制过程中发现空闲region区域不够用时会触发full gc。

在这里插入图片描述
mixed gc阶段分为四步

初始标记

暂停所有用户线程,执行gc线程,标记每个gc root的直接引用对象

并发标记

同CMS并发标记

最终标记

同CMS重新标记

筛选回收

筛选回收是根据GC的停顿时间来决定的。首先G1会按照堆中的所有region的回收成本做一个排序,在GC停顿时间内去回收成本更低的区域。打个比方。4096M堆内存当中有800个老年代的region区域,并且都已经存放满了垃圾对象。按照GC停顿时间默认200ms来计算,当回收600个region已经达到停顿时间时,就不会再继续回收了,这部分剩余的垃圾对象就会等到下一次mixed gc再进行回收。回收的过程主要通过复制算法实现,所以就不会像CMS垃圾回收器一样还存在内存碎片的产生。

full gc

停止其他用户线程,采用单线程进行收集,该过程耗时是非常长的。

对于G1来说,停顿时间越短越好吗?

其实并不是的,默认的200ms是一个规范值,可以根据自己的业务场景对这个停顿时间做一个调整,上下浮动。可能有的同学会说,照你这么那我直接调成10ms或者20ms那不是更好吗?其实不是的。
当你调整的GC停顿时间非常短,那么Mixed gc回收的老年代区域以及young gc回收Eden区域其实是会更少的。因为G1为了满足你的停顿时间,那么就会舍去回收更多的垃圾对象来满足你的要求。
这么做,开始可能不会有特别严重的影响,因为Eden区域是可以动态扩充的,而且老年代开始也有空闲空间去存放这些没来得及回收的垃圾对象。但是当系统运行一段时间,垃圾对象越来越多,在做gc的过程当中挪动对象找不到可用的region区域时,那就完蛋了。这个时候,为了释放出更多可用region区域,会触发full gc,采用单线程来进行回收,回收的过程就会变得很慢了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值