四、垃圾回收器

目录

垃圾回收器

垃圾算法

CMS垃圾回收器(concurrent mark  sweep)

三色标记

写屏障

卡表和记忆集

卡表:

G1收集器

CMS的缺点

Region

G1收集过程

MaxGCPauseMills的设置

G1的垃圾回收分类

YoungGc:

MixGc:

FullGc:

G1参数

为什么CMS用增量更新,而G1却用初始快照呢?


垃圾回收器

目前市面上的垃圾回收器有以下几种:

我们主要看 parnew+cms   和  g1

垃圾算法

常见的三种垃圾算法就三种:

  1. 标记-复制:目前parnew就是利用这种方式。eden+s1+s2。虽然没有碎片,但是会浪费空间,s2就浪费了啊。
  2. 标记-清除:针对垃圾直接清除。虽然很快,但是会产生碎片。
  3. 标记-整理:针对垃圾直接清除,但是会对碎片进行整理,这样就能整理出来一块大的空间。缺点就是内存移动,会stw。

CMS垃圾回收器(concurrent mark  sweep)

cms应用在老年代,并且采用标记-清除算法。 

但是我们可以通过开启两个参数:

  1. UseCMSCompactAtFullCollection。是Java HotSpot VM的一个参数,用于控制是否在使用并发标记-清除(CMS)垃圾收集器进行完全垃圾收集时进行压缩。  这个参数的默认值是true,这意味着在进行完全垃圾收集时,JVM会尝试压缩堆,以减少内存碎片。这可以提高长时间运行的应用程序的性能,因为它可以减少因内存碎片导致的垃圾收集暂停。  然而,压缩堆会增加垃圾收集的暂停时间,因为JVM需要移动对象以压缩空闲空间。如果你的应用程序对暂停时间非常敏感,你可能想要禁用这个选项。你可以通过在启动JVM时添加-XX:-UseCMSCompactAtFullCollection参数来禁用这个选项。  请注意,这个参数只适用于使用CMS垃圾收集器的JVM。在其他类型的垃圾收集器(如G1或Parallel GC)中,这个参数可能没有效果。
  2. CMSFullGCsBeforeCompaction 。 这个参数控制在进行压缩之前需要进行多少次完全垃圾收集。例如,如果这个参数的值为4,那么JVM会在每4次完全垃圾收集后进行一次压缩。这个参数的默认值为0,这意味着在每次完全垃圾收集后都会进行压缩。

我们看一下cms的回收过程:

这里需要注意:之所以有并发,就是为了提升用户体验,减少stw。

  1. 初始标记:会产生stw,然后针对gcroot直接引用的对象,这就很快,因为是直接引用,并不会传递。这个过程为什么需要stw呢,如果不stw,那么程序一直运行,栈不断变化,这个初始标记做不完。
  2. 并发标记: 用户线程和gc线程同时在运行。但是这个步骤不需要stw,这是为啥呢,会出现什么问题呢?
    1. 之所以不需要stw   主要是考虑用户体验,但是堆的空间很大,如果stw,那么将会占用太多的时间。
    2. 那么会出现什么问题呢?
      1. 问题一:原来是垃圾的对象,现在变成了非垃圾。 (三色标记)
      2. 问题二:原来不是垃圾的对象,现在变成了垃圾。(浮动垃圾)
      3. 问题三:并发标记失败。并发标记失败指的是,当并发标记时,由于gc线程和用户线程同时在执行,因此会不断地创建新的对象,此时old区的空间已经不能满足了(因为fullgc一般是在old区使用了92%的时候发生),就会产生并发失败,此时将会stw,然后启动serial-old垃圾回收器。这个将非常非常的慢。
  3. 重标记:此阶段会stw。然后进行三色标记。修正并发标记阶段因为用户线程继续运行而导致的标记状态变化。
  4. 并发清除。此阶段不会stw。也就是说用户线程和gc线程同时执行。这块有个问题,如果在并发清除的时候,有新的对象,但是这对象肯定是没有被标记,那么会不会删除呢?
    1. 答案是不会,因为此阶段,新对象将设置为黑色,默认非垃圾。

三色标记

在CMS(Concurrent Mark Sweep,即并发标记清除)垃圾回收器中,三色标记是一种用于追踪和标记对象存活状态的方法。这种方法将对象分为三种颜色:  

1. 白色:表示尚未被访问的对象,这些对象可能是垃圾,可能会在垃圾回收过程中被清除。

2. 灰色:表示已经被访问,但其引用的对象尚未被完全访问的对象。这些对象在当前阶段不会被清除。

3. 黑色:表示已经被访问,且其引用的所有对象都已经被访问的对象。这些对象在当前阶段不会被清除。  

在垃圾回收过程中,首先将所有对象标记为白色,然后从根对象开始,将访问到的对象标记为灰色,然后逐步访问灰色对象引用的对象,将其标记为灰色,同时将已访问的对象标记为黑色。这个过程一直持续到所有的灰色对象都被访问并标记为黑色,此时所有的白色对象就是垃圾对象,可以被清除。

由于用户线程和gc线程在同时进行,因此会出现上述问题:

1、多标:多标指的是,被标记过的对象,由于用户线程的存在,多一段时间变成了垃圾对象,但是由于被标记过,因此在重标记阶段,不会被重新标记,以至于当前对象一直被jvm认为是非垃圾对象。

        这种情况一般来说没有什么大的问题,无非是一些垃圾对象在这次gc的时候没有被清理,但是下一次gc基本上就会把他清理掉。

2、漏标:指的是在并发标记阶段,由于用户线程的运行改变了对象的引用关系,导致某些应该被标记为存活的对象没有被标记,从而在清除阶段被错误地回收。举个例子,加入目前A引用B,B引用C,当正在标记B的时候,此时B为灰色,A为黑色,C为白色。正常来讲C将来会标记为黑色。但是由于用户线程的存在,可能将B到C的引用断开,而增加A到C的引用。此时由于B不在引用C,因此C将不会被标记。那么在重标记阶段,为了提高效率,jvm将只会重新标记灰色节点,而针对黑色节点不会重新标记,这也就导致了C节点将永远不会被标记到。从而,在并发清理的阶段,由于C为白色,那么C对象将会被清除。

        解决办法:

        1、增量更新。

                当黑色对象增加了一个引用指向白色对象,那么jvm将会将这个黑色节点记录下来,当并发标记结束时候,针对这些黑色节点在重新标记。基本上可以认为,当黑色重新引用白色,这个黑色的节点变为灰色。

        2、初始快照(SATB)。

                这个和增量更新相反,当B对象不在引用C的时候,通过写屏障,将C的对象放入一个队列中。然后再重标记阶段,将这个队列中的对象开始进行标记,且标记为黑色对象。因此使用初始快照的方式,这些断了对象,可能变成了浮动垃圾。      

   

写屏障

从上述的并发阶段产生的问题来看,漏标和多标都是因为用户线程对某个对象进行赋值操作。jvm利用写屏障来将对象写操作进行切面。有点类似于aop。主要功能是在真正用户代码的前后加入屏障逻辑。可以参考下面c++源码。

卡表和记忆集

总的来说,卡表和记忆集其实是一回事,卡表是记忆集的一种实现方式。其真正的目的是为了解决在gcroot扫描的时候,避免跨代扫描问题。

在分代gc回收器中,用的是卡表这种概念。而在g1中用的是记忆集。

卡表:

在分代垃圾回收器中,当发生ygc的时候,需要去从gcroot遍历young区的每一个对象。但是有些young区的对象并没有直接和gcroot关联,而是与old区的一个对象进行关联。那么怎么办呢,ygc可能将old区也扫一遍吧,那岂不就是成了fullgc了。

因此jvm将old区划分成一个一个的小块,每一块称之为card(512B),所有的card连起来就是一个cardtable【但是cardtable维护在年轻代】。在jvm中,cardtable用一个数组来表示。当card中只有存在任何一个对象指向young区,那么这个card就标记为dirty。当younggc进行扫描的时候,除了扫描gcroot直接引用的young区之外,还需要扫描所有dirty的card,从而能扫描到对应的young区对象。

那么卡表什么时候进行更新的呢?当然是赋值的时候,当针对对象进行赋值的时候,通过写屏障,对card进行更新。

G1收集器

CMS的缺点

G1的出现是为了解决大内存空间回收慢的问题。假如我们还用CMS,在现有生产环境下,动辄10几G,几十G内存。那么在这种情况下,一次fullgc的stw将会非常长,且当fullgc之后,如果还要进行内存碎片的整理,那么耗时将是不能容忍的。

另外cms有一个致命的缺点——并发失败。一旦出现并发失败,将会启动-serial-old垃圾回收器,这个回收器是单线程,将非常慢。

Region

G1垃圾回收器会将堆内存划分成一个一个小格子,每个小格子叫做一个Region。至于jvm将heap划分成多少个Region,这个可以通过参数-XX:G1HeapRegionSize来调整Region的大小,那么Region的个数即为HeapSize/RegionSize。

在G1上,还是沿用了之前的年轻代和老年代。但是这仅仅是逻辑上的概念,每个Region即有可能是年轻代,也有可能是老年代。这个不确定的,是会变得。

在默认情况下,整个堆年轻代的占比为5%(当然这个比例可以通过参数-XX:G1NewSizePercen来调整,但是最多不能大于60%),老年代的空间不一定是多少。随着不断地存放,eden区的Region还可以去增加的。  在年轻代的5%里面,也分eden区和s区,也是按照8:1:1。除此之外,G1还有一个类型为Humongous,表示大对象区。这个大对象区什么时候用呢,正常来讲一个新对象首先会放到Eden区,当对象的大小超过了 EdenRegion的50%的空间的时候,就会放入到Humongous区

G1收集过程

G1的回收算法:G1不论是老年代还是年轻代,都采用了标记-复制算法,因为他有很多个空闲的Region,当一个Region被回收的时候,直接复制就好了。所以从另一个角度看,也就相当于是整理了一下。

1、初始标记:stw

2、并发标记

3、最终标记 :stw

4、筛选回收。用户可以设置-XX:MaxGCPauseMills(默认是200ms) 来制定回收计划。也就是说,必须要在这个时间范围内完成gc。但是在这个时间内,有可能有些Region回收不完,至于哪些回收价值比较高的Region才会回收,什么Region价值高呢,从G1的名称可以看出,G1=Garbage First,那就是垃圾比较多的Region。G1会优先选择垃圾对象最多的Region进行回收,这样可以最大限度地回收内存。这些垃圾比较多的Region就叫做CollectionSet。

MaxGCPauseMills的设置

MaxGCPauseMills 默认值为200ms。如果我们 设置10ms会有什么问题呢?如果设置为10ms,那么在筛选过程,jvm收集的垃圾会非常少,这会导致大部分Region不能回收,那么就会导致FullGc。

G1的垃圾回收分类

YoungGc:

eden区默认是5%,当5%放满了,正常应该触发YoungGc,但是对于G1,不一定会触发,G1会计算当前eden区的回收时间是否接近MaxPauseMills。 如果预估的时间远小于MaxPauseMills,那么说明当前分配的Eden区太小了,那么将不会触发YoungGc,而是继续扩大Eden的Region个数。只有当预估时间接近MazPauseMills,则进行YoungGc。

在进行YoungGc的时候,依然采用GcRoot扫描,但是与CMS不同,G1的扫描的时候,在处理跨代问题采用的是RememberSet。例如当前从GcRoot开始,发现引用的对象在老年代,这种情况CMS采用判断Card是否为dirty来决定是否扫描。而G1则不需要扫描老年代,而是判断Eden区的RememsetSet是否有对象被老年代引用,如果有,则除了gcroot之外,还需要扫描RememberSet里面的对象。

MixGc:

在G1中有一个参数 -XX:InitiatiingHeapOccupancyPercent。当Old区的使用占比大于参数值设置,则会进行 MixGc。而MixGc除了回收年轻代还会回收老年代。

FullGc:

触发时间:当没有足够的Region区使用的时候,就是产生FullGc。

那么FullGc是什么意思,这个和CMS中的Full不一样,而是和CMS的【并发失败】一样。这时候会用一个单线程去收集垃圾。

G1参数

为什么CMS用增量更新,而G1却用初始快照呢?

CMS使用增量更新的时候,在重标记的时候会再次扫描这些节点,而初始快照,只需要标记为黑色就可以了,效率比较高,如果G1也用增量更新,那么G1就需要重新扫描,但是G1的分代比较多,意味着跨代比较多,因为如果扫描就会效率低。

但是G1的初始快照会引起浮动垃圾。不过,G1本身就会有垃圾未清理的情况,例如那些在CollectionSet之外的待回收的Region就是浮动垃圾啊。所以这个缺点G1是能容忍的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值