Java垃圾回收器

Java垃圾回收器

JVM垃圾回收算法只是一种思想,一种方法论,而垃圾回收器是垃圾回收的具体实现。

1、Serival收集器

串行收集器,是最古老,最稳定的垃圾收集器,效率也比较高。

  • 单线程收集
  • 需要暂停用户线程
  • 新生代复制算法,老年代标记整理算法
  • 参数控制:-XX:+UseSerival串行收集器

由于是单线程的原因,回收速度比较慢,可管理的堆内存空间比较小。

2、ParNew收集器

多线程版本的Serival收集器

  • 吞吐量比较高:并发收集,利用多核CPU的优势,大幅度降低gc时间。
  • 需要暂停用户线程
  • 新生代采用复制算法。,老年代标记整理算法
  • 参数控制 -XX:+UseParallelGC-XX
  • 适用于需要有效利用CPU资源,容忍暂停的场景

上述的两种收集器在进行垃圾回收的过程中,都需要stop the world【暂停用户线程】,在如今的B / S系统的服务器上,长时间的不响应用户是无法忍受的。因此逐渐衍生出了不需要长时间stw的垃圾收集器

3、CMS 垃圾收集器

CMS : Concurrent Mark Swap 是一种追求最短停顿时间的垃圾收集器。基于标记–清除算法,针对老年代。

3.1 回收步骤

主要分为标记和清除两个步骤。
在这里插入图片描述

为了减缓Stop the word的问题,使得最大程度上垃圾回收线程和用户线程并行,将整个标记清除分为以下几个阶段。

  • 初始标记:需要stop the world ,仅仅标记与GC Roots直接相连的对象,因为这部分对象毕竟是少数以及一些如OopMap的优化技术,因此用户线程暂停时间较短。
  • 并发标记:遍历所有与GC Roots间接相关联的对象也就是遍历整个对象构成的图,这部分对象比较多因此时间比较长,但是标记线程可以和用户线程并发执行,不需要Stop the world。但是这必然存在一个问题,在用户线程执行的过程中又会产生一些垃圾,这部分可能会标记不到。正如你妈妈在打扫房间的时候,你依旧在仍纸团。
  • 重新标记:重新标记一次,标记在并发标记过程中,用户线程产生的垃圾对象,这一步需要Stop the world。因为在并发标记过程中产生的垃圾相对较少,因此这一步stop the world的时间也比较短。
  • 并发清除:清理掉之前所标记的对象,因为需要清理的对象比较多,这一步比较耗时,因此将用户线程和垃圾回收线程并发执行,不需要stop the world。

为什么年轻代不适用CMS呢?主要是因为CMS是基于标记清除的不适合年轻代。

3.2 存在的问题
  • CPU敏感:CMS默认使用(CPU数量 + 3) / 4个线程来进行垃圾回收,因此当服务器的CPU个数小于4个话,对于CPU来说是一个较大的负担。但如今服务器的CPU数量基本都大于四个。
  • 浮动垃圾问题:因为在并发清理的过程中没有stw,用户线程还是会产生一部分垃圾,这部分垃圾也需要占用一一定的内存空间,因此需要进行空间预留,因此必须提前进行垃圾回收,不能等老年代几乎填满了才进行垃圾回收。如果这样的话,在并发标记阶段用户线程想申请内存就申请不了了。JDK6之后大概老年代空间使用了92%左右就会触发垃圾回收。但是还存在一个问题,万一有什么大对象的产生,预留的空间放不下,而且老年代由于采用的是标记清清除算法,最后老年代也放不下,此时就需要切换垃圾回收器为SerivalOld这么一个单线程且完全STW的垃圾收集器进行垃圾回收。 这个在生产环境其实是有很大风险的。
  • 内存碎片问题:因为基于标记—清除算法,本来就有这个问题,但为什么要使用这个算法呢?因为CMS绝大多数情况都和用户线程并行,因此如果使用整理或者复制算法会动态的改变对象的位置,为了解决这个问题,在需要分配大对象的时候,使用一次整理算法。

上面的几种垃圾回收器都关注的是如何快速的整理整个堆内存,但是仔细想一下,只要垃圾回收的速率高于对象分配的速率,是完全不影响程序的正常运行的,因此不需要对整个堆内存进行回收,只要使得回收产生的空闲空间大于程序需要的空间即可。另外可以事先定义一个最大停顿时间,只要时间到了就停止回收。

4、G1垃圾收集器

在CMS垃圾回收器收集的过程中,有这么一个极端 场景因为新生代仍然使用的复制算法,在复制的时候Survivor区放不下了,因此需要晋升到老年代,而老年代由于采用CMS垃圾收集器,多次标记清理导致碎片太多,也存放不下,此时会进行一次Full GC。 一次简单的 Major GC,竟然能演化成耗时最长的 Full GC。而且这个停顿时间是不可预知的。对于一些对响应时间有严格要求的业务,这个是无法忍受的。

G1的由来其实很简单,从之前的单线程收集,到多线程收集,再到CMS多步骤标记,尽可能的减少stw,但是随着堆内存的越来越大,即使重新标记也需要耗费不少时间,需要换一种思想进行垃圾回收了。

G1的思路很简单,它不要求一次性把整个堆内存都清理的干干净净,只要在指定的时间内认真清理就行了。

G1 : Gabage First 开启参数-XX:+UseG1GC。该垃圾回收器将整个堆内存化整为零,划分为一个小小的region,region大小的范围为1–32MB具体根据参数-XX:G1HeapRegionSize来进行设置,且应为2的次幂。G1仍然使用分代的思想,每一个region可以划分为Eden,survival(From,To)区,为了存储大对象又引入了一个Humongous这么一个区域,这个区域也属于老年代,当对象大小超过region的一半的时候,会分配到这个区域。划分成同一种的类型的region区域可以是不连续的,因此G1收集器看来新生代和老年代是逻辑的,如下图所示:
在这里插入图片描述
对G1来说,每次回收的时候,不需要回收整个堆,回收的对象成为Collection Set 简称CS。

G1的垃圾回收过程和CMS的类似,也分为初始标记,并发标记,重新标记 最后一步筛选回收,不过最后一步是需要stw的,因为G1仅仅回收效益最大的几个region,不需要对整个堆内存进行回收,再加上多线程回收所以消耗的时间很短,所以即时STW的话,时间也很短。另外G1的每个regin的标识是可以动态变化的,因此在复制过程中,为了方便不一定真正的需要复制对象,有时候只是将标识改变一下即可。

4.1、G1的回收流程

年轻代回收:刚开始eden都是空的,随着对象分配,eden满了,就要开始进行垃圾回收。

  • 构建CS,决定回收哪些region。
  • 扫描GC root
  • 更新 RS,处理 dirty card queue 中的卡页,更新 RSet。之前只是将脏卡加入了队列,还没有更新。
  • 处理RS,找到相关的跨代引用。
  • 活对象copy
  • 处理引用:处理 Soft、Weak、Phantom、Final、JNI Weak 等引用。结束收集。

老年代回收:

5、一些实现细节

5.1三色标记法

之前的标记过程是stw的,因此在标记过程中,对象与对象的引用不会再发生改变,但是由于CMS和G1都在这一步都不需要STW,因此必须解决并发标记过程中对象的引用发生变化问题,为此引入了三色标记法。
在并发标记过程中,遇到的对象,按照自身及其子对象是否标记过,分为三种颜色

  • 黑色:该对象所有的子对象都已经被扫描过,或者自身是根对象。
  • 灰色:自身已经被扫描过,但还存在至少一个子对象没有被扫描。
  • 白色:自身还没有被扫描,如果垃圾回收器扫描完,还有对象的颜色是白色的,说明就是垃圾。

那么三色标记法如何解决并发标记过程中对象的引用发生改变的呢?对象的引用关系发生变化会导致两种问题:1、原本死亡的对象标记为存活的,2、原本存活的对象标记为死亡的,第一种的影响不大,但第二种必然造成程序错误。图示问题的产生:

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
为了解决这个问题,引入了两种解决方案:

  • 增量更新:对于上述情况,在进行回收的时候,发现A的子引用C是一个白色对象,会重新进行一次并发标记。这是CMS使用的算法。它关注的是引用的增加,即A到C多了一条引用。

  • 原始快照:上图中B到C的引用消失,这个信息将会记录下来,标记前后两次的快照在GC的堆和栈中进行对比,这样就可以确定哪条引用消失了。这是G1使用的算法,它关注引用的减少。

5.2、跨代 / 跨region 引用

因为垃圾收集算法的设计过程中是将堆内存进行分代的,由于新生代的对象朝生夕死,垃圾回收频繁,若存在老年代的对象对新生代对象的引用,按照理解应该扫描整个老年代,来判断新生代的垃圾有没有被老年代所引用,这样才可以进行放心回收。但是因为新生代的回收是比较频繁的,这样每次全量扫描老年代是一件比较耗时的操作,为此提出了一些优化措施。

把一个regin划分为等大的区域,每个区域对应一个cardtable,当该区域存在对其它区域的跨region引用的话,则把这个cardtable记录在另一个区域的RSet中。简单而言就是每一个region都有一个数据结构用来保存哪块区域对自己区域中的对象存在引用。到时候回收的时候,只需要扫描对自己有引用的区域即可,避免了整个堆的扫描,是一种空间换时间的思想。
在这里插入图片描述
记录下来是一方面,但是还需要维护它的状态更新,那么如何知道它什么时候变化或者更新呢?使用write barrier 进行维护。如果发生了变化,这个cardtable就称为一个dirty,存入一个queue中,

5.2.1 CardTable

一个抽象的概念,用来存标识哪块内存存在对其他区域(跨代)的引用。实现方式可以有很多,在精确和效率之间取舍;常见的有三种实现:

  • 字长长度:即处理器的一次寻址范围,指明该区域存在跨代引用。
  • 对象精度:指明该对象是跨代引用
  • 卡精度:一块小区域存在跨代引用

其中卡精度的是一种使用最频繁的实现方式,也称为卡表。

5.2.2 RSet

卡表可以理解为一个byte数组,数组的每个元素代表固定范围的内存范围,如果该范围中存在跨代引用,则此处元素的值为1,反之为0,这样就可以知道哪块内存区域存在跨代引用,无需遍历整个老年代。

5.3 安全点与安全区域
5.3.1安全点

JVM在进行垃圾回收的时候,第一步往往是标记与GCRoots直接关联的对象,这一步是需要STW的,随着堆内存的不断变大,这些对象也不断增多,为了减少STW时间,在类加载完成后计算机出对象的具体偏移地址处存在这种引用,并且使用一种称为OopMap的数据结构存储起来,这样只需遍历OopMap即可,无需遍历整个堆内存。
但是随着程序的运行,这个OopMap是需要进行更新的,每一条指令都可能导致OopMap的变化,但是为每一条指令都生成Oopmap的话消耗比较大,因此只是在某些特定的点生成了OopMap,这些点便称为安全点,JVM必须在用户线程到达安全点的时候才可以进行垃圾回收,因为安全点才有OopMap。安全点的设置通常在循环跳转,异常跳转,方法调用等地方。
用户线程停下来的方式有两种:1、主动轮询,2、抢占式中断,主流虚拟机都采用主动轮询来暂停用户线程。

5.3.2 安全区域

安全区域是指在该区域内,对象的引用关系不会发生变化,针对处于阻塞或者等待的用户线程。当线程进入安全区域后,会标识自己进入了安全区域,GC线程就无需关注该用户线程了,可以进行垃圾回收了。线程要离开安全区域,必须等待GC完成根节点枚举否则需要一直等待。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值