JVM之GC

本文详细解析了Java垃圾回收(GC)机制的核心概念,包括GC的三大任务:确定哪些内存需要回收,何时回收,以及如何回收。介绍了常用的垃圾回收算法,如标记清除、复制算法、标记整理和分代收集算法,以及各种垃圾收集器如Serial、ParNew、ParallelScavenge、CMS和G1收集器的原理与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

初级 Java 程序员步入中级程序员的有一个无法绕过的阶段------GC(Garbage Collection)。作为 Java 程序员,说实话,很幸福,不用像 C 程序员那样,时刻关心着内存,就像网上有句名言------生活从来都不容易,只不过是有人替你负重前行!是的,GC 在替我们做这些脏活累活,GC 想让我们把精力都放在业务上,而不用每时每刻都在想着内存。现在,GC 也是每个语言的标准配置了。不然谁会去使用这个语言呢?

然而,作为一个合格的程序员,对底层的好奇是进步的动力,如果一个程序员失去了好奇心,那就可以说他在程序员这条道路上就结束了。

难道我们不好奇 GC 到底是怎么做的吗?接下来,我们就分析 GC 做了哪些事情。

实际上,GC 主要做3件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

说到底,GC 就是做这3件事情,如果你能解决这3个问题,那么你也可以实现一个 GC。

那我们就一个一个问题来看看。

1. 哪些内存需要回收

还记得我们之前分享的关于 JVM 运行时数据区吗?有堆,有栈,有方法区(永久代),还有直接内存,还有 PC 寄存器。其中,GC 的主要战场就是堆,当然,方法区也是需要 GC 的。但重点还是堆。

我们知道,堆中内存是共享的,基本所有的对象都是在堆中创建。当一个对象不需要使用了,理论上我们就需要释放他所占用的内存。

问题来了,如何分辨一个对象不需要使用了呢?答案是:不可能被任何途径使用的对象。也就是说他没有了任何引用。我们知道,引用在栈中,实例在堆中,当一个实例没有了指向他的引用,我们认为,这个实例就需要被清除并释放他所占用的内存了

那么 GC 是如何实现的呢?一般而言有2种方法:

  1. 引用计数法(有缺陷,无法解决循环引用问题,JVM 没有采用)
  2. 可达性分析(解决了引用计数的缺陷,被 JVM 采用)

什么是引用计数法呢?

给对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

虽然乍看这个算法简单,效率也高,但有一个问题这个算法无法解决,就是循环引用。试想一下:A 对象引用了 B,B 对象也引用了 A,但 A 和 B 都不被别的地方使用,也就是说,实际上这两个对象是垃圾对象,但是由于他们互相持有引用,导致他们的引用计数器都不为0,因此系统无法判断是垃圾,也无法回收他们。

所以,在现在的 JVM 中,是没有使用这个算法的。我们知道就行。

引用计数法不行,那就再说说可达性分析算法。

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,所有所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(也就是对象不可达)时,则证明此对象是不可用的。如下图所示,obj5 , obj6, obj7 虽然互相有关联,但是他们到 GC Roots 是不可达的,所以他们将会判定为是可回收的对象。

可达性分析算法

那么哪些对象可以作为 GC Roots 对象呢?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中 JNI (即 native 方法)引用的对象。

2. 什么时候回收?

注意:即使是在可达性分析算法中不可达的对象,也并非是"非死不可的",这时候他们实际上是处于 “缓刑” 阶段。因为要真正宣告一个对象的死亡,至少需要经历两次标记过程:

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。注意:当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。也就是说,finalize 方法只会被执行一次。
=========================================================
如果这个对象被判定为有必要执行 finalize 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的 Finalizer 线程去执行它。注意:如果一个对象在 finalize 方法中运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃。
=========================================================
finalize 方法是对象逃脱死亡命运的最后一道关卡。稍后 GC 将对队列中的对象进行第二次规模的标记,如果对象要在 finalize 中 “拯救” 自己,只需要将自己关联到引用上即可,通常是 this。如果这个对象关联上了引用,那么在第二次标记的时候他将被移除出 “即将回收” 的集合;如果对象这时候还没有逃脱,那基本上就是真的被回收了。

这里需要注意的一点就是:一个对象如果重写了 finalize 方法,那么这个方法最多只会被执行一次。

建议:如非必要,不要重写该方法。可以使用 try-finally 代替,此方式更好,更及时。同时注意:在 Mysql 的 JDBC 驱动中,com.mysql.jdbc.ConnectionImpl 就实现了 finalize 方法,作用是:当一个 JDBC Connection 被回收时,需要进行连接的关闭,如果开发人员忘记了关闭,则在 finalize 方法中进行关闭。但是,由于其调用的不确定性,这不能单独作为可靠的资源回收手段。

到这里,我们知道了什么时候进行回收:如果一个对象重写了 finalize 方法且这个方法没有被 JVM 调用过,那么这个对象会被放入一个队列等待被一个低优先级的线程执行 finalize 方法,如果在这个方法中对象不能自救,则这个对象在第二次标记过程中就会被标记死亡,等待 GC 回收。

3. 如何回收?

如何回收,这个问题非常的大,涉及到各种垃圾回收算法,各种垃圾收集器。限于本篇的篇幅,楼主将不会在这篇文章里深入探讨,这里只会列出一些大纲,这些大纲将是后面文章的摘要,我们将在后面的文章中深入探讨如何回收。

那么,有哪些摘要呢?

3.1 垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)

这些算法是 GC 的基础,所有 GC 的实现都是基于这些算法来清除无用对象,然后释放内存空间。我们将会在后面的文章一个一个讲解。

3.2 有哪些垃圾收集器

  1. Serial 串行收集器(只适用于堆内存256m 以下的 JVM )
  2. ParNew 并行收集器(Serial 收集器的多线程版本)
  3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
  4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
  5. G1 收集器(JDK 9 的默认 GC)

3.3 有哪些GC

  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

好,以上就是如何回收的大纲,我们将在后面的文章中慢慢讲解。

总结

这篇文章主要总结了什么是 GC ,以及 GC 的作用,GC 主要做了3件事情,哪些内存需要回收,什么时候回收,如何回收。我们知道了 GC 通过可达性分析知道了哪些内存需要回收,那什么时候回收呢?执行 finalize 方法后如果还没有复活,将被回收。第三个问题:如何回收呢?这个问题是一个大课题,我们只是列出了一些大纲,比如有哪些垃圾收集器,有哪些垃圾算法,有哪些 GC 过程。这些细节我们将在后面慢慢讲解,逐步深入。


二、前言

限于上篇文章的篇幅,我们留下了一个问题 : 如何回收? 这篇文章将重点讲述这个问题。

在上篇文章中,我们也列出了一些大纲,今天我们就按照那个大纲来逐个讲解。在此,我将大纲复制过来。

垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)

有哪些垃圾收集器

  1. Serial 串行收集器(只适用于堆内存256m 以下的 JVM )
  2. ParNew 并行收集器(Serial 收集器的多线程版本)
  3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
  4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
  5. G1 收集器(JDK 9 的默认 GC)

有哪些GC

  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

1. 有哪些垃圾回收算法

  1. 标记清除算法
  2. 复制算法
  3. 标记整理算法
  4. 分代收集算法(堆如何分代)

1. 标记清除算法

GC 中最基础的算法就是标记-清除算法,所谓标记清除,就是通过可达性分析,标记哪些是垃圾对象,然后清除。之所以说是最简单的算法,是因为后面的几种算法都是基于它的。

但是这个算法有2个不足之处:1. 碎片问题,标记清除之后会导致大量内存不连续的碎片,空间碎片太多会导致分配大对象时无法找到足够的连续内存从而提前触发 Full GC (此 GC 严重影响应用性能)。2. 效率问题,标记和清除这两个过程的效率都不高。我们通过一幅图来看看标记清除的算法:

标记清除算法示意图

可以从上图看出,回收后,出现了大量的内存不连续的内存块。

2. 复制算法

为了解决效率问题,人们发明了一种复制算法(Coping)。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的用完了,就开始垃圾回收,将有用的对象复制到另一个空闲的内存上,清空之前使用的内存块。这样使得每次都对整个半区回收,而且也不用考虑内存碎片问题,只需要移动堆顶指针,按顺序分配即可,实现简单,运行高效。

但是凡事都是有缺点的,复制算法的缺点就是内存缩小到了原来的一半,无法充分利用内存空间。

总是有取舍的。

现代的所有商业虚拟机都是采用这种算法来回收新生代。基于统计学,人们得出99% 的对象都是朝生夕死的,所以不需要留出那么大的空间保存存活的对象,也就是不要1:1 的比例来划分内存。

通常的做法是:

将内存分为一个较大的Eden(伊甸园)空间和两块较小的 Survivor(幸存区)空间,每次使用 Eden 和其中一块 Survivor ,当回收时,将 Eden 和 Survivor 还存活着的对象一次性的复制到另外一块 Survivor 空间,最后清理掉 Eden 和刚刚使用的 Survivor 空间。Hotspot 默认的比例是 8:1:1,也就是说,每次新生代可用内存空间为新生代总空间的90%,只有10%的内存会被浪费,从一定程度上解决了复制算法浪费空间的问题。

当然,98% 的对象可回收只是一般的情况下,我们无法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时怎么办呢?肯定需要依赖其他内存(老年代)进行所谓的分配担保(Handle Promotion)。

什么是分配担保呢?

如果另外一块 Survivor 区域没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。当然,具体细节这句话无法详细说明,我们将会在之后阐述具体细节。

3. 标记整理算法

从上面我们可以看出,复制算法的效率很高,请注意,该算法只有在对象存活率较低的时候(98% 对象可被回收)才能体现出效率。而如果一次 GC 活动之后,存活对象很多,那么就需要复制大量的对象,很明显,会导致效率不高;更关键的是,还需要额外的空间进行分配担保

所以,存活对象时间很长的老年代一般不使用该算法。

根据老年代的特点,一般使用“标记-整理(Mark-Compact)”算法,标记过程仍然与 “标记清除” 算法一样,但我们知道,标记清除算法会产生大量的内存碎片,对性能影响很大,所以标记整理算法后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象像一个方向移动,然后清理掉边界之外的内存。也就是将那些原来散落的对象移动在一起,让碎片不再存在。

可以说,标记整理算法相对于标记清除算法牺牲了一些性能,但却避免了内存碎片的产生,在大部分场合,可抵消掉整理过程中产生的性能损耗。

4. 分代收集算法

上面我们提到了几个名词,新生代,老年代,这些就是分代算法中名词。分代算法最主要的就是根据对象存活周期的不同将内存分成几块,一般是把 Java 堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理” 或者 “标记整理” 算法来进行回收。

2. 有哪些垃圾收集器

上面说的这些算法都是实现垃圾收集器的基础。

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

Hotspot 虚拟机所包含的所有收集器如下图:

Hotspot 所有 GC 组合

从上图中看到,一共有6种 GC 组合(忽略 G1 和 为CMS备份的 SerialOld 组合 )。

  1. Serial + Serial Old
  2. Serial + CMS
  3. ParNew + CMS
  4. ParNew + Serial Old
  5. Parallel Scavenge + Serial Old
  6. Parallel Scavenge + Parallel Old

大家看到这里,一定有个疑问,为什么需要这么多垃圾收集器?

答案是:没有任何一种垃圾收集器是完美的,没有任何一种垃圾收集器适合所有的应用情况。

每个应用都需要自己的特定垃圾收集器,因此,可以说,GC 调优是门艺术,没有放之四海皆准的 GC。需要工程师们去根据应用的特性不断调优。

这么多 GC ,限于篇幅,我们将在下文中慢慢解释。
这里只是列出一个大纲。

接下来我们将说说关于 GC 的一些概念,方便阅读后面的关于 GC 处理器的文章。

3. 有哪些GC

  1. Young GC(又称 YGC,minor GC,年轻代 GC)
  2. Old GC (老年代 GC,只有 CMS 才会单独回收 Old 区)
  3. Full GC(又称 major GC)
  4. Mixed GC(混合 GC,G1 收集器独有)

关于这些 GC 的分类,R 大一个回答比较清楚:Major GC和Full GC的区别是什么?触发条件呢?

从大的方面讲,GC 只分为两种,一种是不收集整个堆,一种是收集整个堆。

Partial GC:并不收集整个GC堆的模式

  1. Young GC:只收集young gen的GC
  2. Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
  3. Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式

Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

1. YGC

YGC 又称 Young GC ,minor GC ,年轻代 GC。顾名思义,该 GC 过程发生在年轻代中。从分代算法中,我们知道,JVM 为了性能考虑,通常将内存区域根据对象生命周期的不同分为年轻代和年老代。

新创建的对象基本上都存放在年轻代(除了一些大对象),因为大多数对象都是很快变成引用不可达,所以大多数对象都在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们称之为 YGC。

什么时候发生 YGC 呢?当 Eden 不够放入新创建的对象时,也就是Eden 区满了,JVM 就会清理Eden 区的空间,将存活的对象放入 to 区,如果 to 区放不下,则直接进入老年代。如果 to 区能放下,则放入 to 区,然后清理掉无用对象,第二次 YGC 时,GC 扫描 Eden 区和 to 区,将这两个区的存活对象放入到 from 区,将 to 区清空(总之一定会保证有一个 Survivor 区是干净的),同样的,如果 from 区放不下,则通过分配担保机制进入老年代。如果 YGC 后,仍放不下新对象,则也通过分配担保进入老年代。

2. Old GC

通常,我们将 Old GC 等同于 Full GC,为什么呢?我们详细解释一下。

什么时候发生 Old GC? 当老年代空间满了的时候。也就是说通常是 YGC 后有很多对象进入到老年代,而老年代无法放下这些对象,这时候就需要对老年代 GC。而通常的 Old GC 其实就是 Full GC 。

3. Full GC

也就是全 GC ,对整个堆和方法区(如果存在)进行 GC。
哪些情况会 Full GC 呢?

  1. System.gc() 方法的调用
  2. heap dump 带 GC
  3. 永久代(方法区)空间不够
  4. 当准备出发 YGC 时,发现之前 YGC 后晋升对象的大小比目前 Old 区的剩余空间大,则不会触发 YGC ,转而直接触发 Full GC。

第四条说到晋升,什么是晋升呢?YGC 后,幸存的对象会放入到 Survivor 区,如果一个对象在多次 YGC 后仍然存活,则进入老年代,这个过程叫做晋升。每次 YGC 后,这个对象的年龄加一。当然,晋升的条件比较复杂。我们后面会详细讲述。

4. Mixed GC

G1 专属GC,这里不准备讲述这个 GC。

总结

到这里,我们解释了3种垃圾回收算法,第四个不算是算法,而是一种设计。还大致讲了5种收集器,并将这个坑留在了后面的文章里,最后讲了一些 GC 术语,YGC ,Old GC ,Full GC 等。

堆内存 = 年轻代 + 年老代 + 永久代
年轻代 = Eden区 + 两个Survivor区(From和To)


三、前言

在上文中,我们介绍了一些 GC 算法,GC 名词,同时也留下了一个问题,就是每个 GC 收集器的具体作用。有哪些 GC 收集器呢?

  1. Serial 串行收集器(只适用于堆内存 256M 以下的 JVM )
  2. ParNew 并行收集器(Serial 收集器的多线程版本)
  3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)
  4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)
  5. G1 收集器(JDK 9 的默认 GC)

再回顾一下我们的那张图吧:

下面我们就一一介绍这些垃圾收集器吧!

1. Serial 串行收集器(只适用于堆内存256m 以下的 JVM )

什么是串行收集器呢?

串行收集器是指使用单线程进行垃圾回收的回收器。每次回收时,串行收集器只有一个工作线程,对于并行能力较弱的计算机来说,串行回收器的专注性和独占性往往有更好的性能表现。串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器。

串行回收器可以说是最古老的垃圾回收器了,主要由2个特点:

  1. 他仅仅使用单线程进行垃圾回收。
  2. 他是独占式的垃圾回收器。

什么是独占式呢?

在串行收集器金进行垃圾回收时,Java 应用程序中的线程都要暂停,等待垃圾回收的完成。这种现象称之为 “Stop-The-World”,他将造成非常糟糕的用户体验,在实时性较高的应用场景中,这种现象往往是不能接受的。

即便如此,串行回收期却是一个成熟且经过长时间生产环境考验的极为高效的收集器。新生代串行收集器使用复制算法,实现相对简单,且没有线程切换的开销。在单 CPU 环境下性能表现良好。

我们可以使用 -XX:UseSerialGC 参数,指定使用新生代串行收集器和老年代串行收集器。注意,当虚拟机在 client 模式下,它是默认的垃圾收集器。

当然还有老年代串行收集器。

老年代串行收集器使用的标记压缩算法,也是一个独占式的单线程的垃圾收集器。由于老年代的垃圾回收通常比新生代垃圾回收需要更多的时间,因此,一旦老年代垃圾回收期启动,系统将停顿很长时间。

即便如此,Serial 老年代处理器也是大名鼎鼎的 CMS 处理器的备用处理器。

2. ParNew 并行收集器(Serial 收集器的多线程版本)

上面我们说 Serial 是单线程的处理器,在单核 CPU 情况下,Serial 是个不错的选择,但现代计算机普遍都是多核,因此需要并行的处理器。

ParNew 就是 Serial 的并行版本。多个线程同时回收,有效缩短垃圾回收所需要的实际时间。

ParNew 是一个工作在新生代的垃圾收集器,他只是简单的将串行收集器多线程化,他的回收策略,算法以及参数和新生代串行收集器是相同的。同时也是独占式的收集器,在收集过程总,应用会全部暂停。但由于并行回收期用多线程回收,因此,在并发能力比较强的 CPU 上,他产生的停顿时间要短于串行回收器。反之,如果 CPU 并行能力弱,不如使用串行收集器。

同时,既然是多线程的,虚拟机给我们提供了指定线程数量的参数 -XX:ParallelGCThreads,一般,最好和 CPU 数量相当,默认情况下,当 CPU 数量小于8,ParallelGCThreads 等于 CPU 数量,当 CPU 数量大于 8时,公式是: 3 + ((5 * CPU——Count)/8)。

3. Parallel Scavenge (PS 收集器,该收集器以吞吐量为主要目的,是1.8的默认 GC)

Parallel Scavenge 收集器,又称 PS 收集器,也是多线程的,和 ParNew 类似,但是,PS 收集器更关注吞吐量。

因此,PS 处理器特意提供了连个参数用于设置吞吐量相关。
-XX:MaxGCPauseMillis :设置最大垃圾收集停顿时间,他的值是一个大于0的整数,ParallelGC 在工作时,会调整 Java 堆大小或者其他一些参数,尽可能的把停顿时间控制在 XX:MaxGCPauseMillis 以内。如果设置的很小,对应的,PS 收集器会将堆设置的很小(小堆比大堆回收快),导致垃圾回收变得频繁,从而降低了吞吐量。

-XX:GCTimeRatio: 设置吞吐量大小,他的值是一个0 - 100 之间的整数,假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n)的时间用于垃圾收集,比如 n 是 19,则系统用于垃圾收集的时间不超过 1/(1+19) = 5%的时间用于垃圾收集,默认情况下,取值为99,即不超过 1% 的时间用于垃圾收集。

注意:PS 收集器是一个自适应的收集器,使用 -XX:UseAdaptiveSizePolicy 可以打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 Survivor 的比例,晋升老年代的年龄阈值将会别自动调整
,以达到在堆大小,吞吐量和停顿时间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆,目标吞吐量(GCTimeTatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。

也许大家也看到了,GCTimeRatio 和 MaxGCPauseMillis 两个参数有冲突的,通常如果减少一次垃圾收集的停顿时间,意味着你的吞吐量就会下降,如果吞吐量设置的很高,那么你的垃圾收集停顿时间又会变大。

Parallel Old

有新生代 Parallel Scavenge 收集器,也有老年代 Parallel Old 收集器,他也是一种关注吞吐量的垃圾收集器。故名思意,他是一种工作在 Old 区的垃圾收集器,并且和 Parallel Scavenge 一起使用。Parallel Old 收集器使用的标记压缩算法。

4. CMS 收集器(该收集器全称 Concurrent Mark Sweep,是一种关注最短停顿时间的垃圾收集器)

我们上面说 Parallel Scavenge 和 Parallel Old 收集器都是关注吞吐量的,而现在说的 CMS 处理器则是关注停顿时间的。CMS 是 Concurrent Mark Sweep 的缩写,意味并发标记清除,从名称上可以得知,他使用的是标记清除算法(缺点是产生内存碎片),同时他又是一个使用多线程并行回收的垃圾收集器。

相对于 Serial Old, Parallel Old 这两个老年代处理器,CMS 比较复杂,为了实现更短的停顿时间,将 GC 的流程更加的细化。

我们仔细思考,GC 有标记和清理两个过程,事实上,清理的过程是不要 STW(Stop-The-World)的,只有在标记的时候,需要暂停所有应用线程,防止引用关系更改。因此 CMS 做了如下的设计:

CMS 工作过程

上图有6个步骤,但大部分书中都是4个步骤,也就是绿色方框中的,注意,其中初始标记和重新标记都是要系统停顿的,而并发标记和重并发清理都是和系统应用程序并发执行的,因此,相对于上面的两个收集器,CMS 收集器的停顿时间要小的多。

那么我们就详细说说这几个步骤。

  1. 初始标记,初始标记仅仅是标记一下 GC Roots 能直接关联到的对象,速度很快。
  2. 并发标记阶段就是进行 GC Roots 的跟踪过程。
  3. 预处理,由于并发标记阶段是和应用程序并发执行的,因此,极有可能会产生大量新生的对象指向老年代的对象,引用关系发生变化,同时,后续的 remark 阶段是独占式的,如果不处理那些新生对象和老年代对象的关系,那么 remark 阶段将非常耗时,严重影响性能。因此,在预处理阶段,将会尽量处理那些变化的老年代对象,默认5秒之内,在这段时间内,CMS 会尽量处理那些变化的对象,特别是新生代中的对象,其实这5秒,实际上是在等待一次 YGC,希望 YGC 能够把那些新生的对象消除,避免后面的 remark 阶段扫描导致长时间暂停。不过,这个功能可以通过 -XX:-CMSPrecleaningenabled 关闭。当然也可以通过参数 CMSScavengeBeforeRemark 强制在此阶段发生 YGC。注意:虚拟机还会预估下次的 YGC 发生时间,尽量不让 remark 阶段和下一次 YGC 阶段重叠,防止停顿时间过长。
  4. 重新标记,为了修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的暂停时间一般会比初始标记时间稍长一些,但远比并发标记的时间短。
  5. 并发清理,没啥说的。
  6. 重置之前的状态。

可以说 CMS 还是比之前的稍微的复杂了一点。同时,CMS 还有3个地方需要注意:

  1. CMS 对 CPU 资源敏感,什么意思呢?由于 CMS 是并发执行的,虽然不会导致应用程序暂,但是会抢夺 CPU 的资源,应用程序的性能会受到影响。默认线程数是 (CPU + 3)/ 4。所以需要妥当设定好 ParallelGCThreads 参数。
  2. 由于并发清理阶段程序会继续运行,会产生大量的对象,如果内存不够,将会出现 Concurrent Mode Failure 同时 Full GC,并使用备用收集器 Serial ,停顿时间将会非常的长。当出现这种情况的时候,使用 -XX:CMSInitiatingOccupancyFraction 的值来设定老年代的空间使用的百分率来触发 CMS,如果 Old 区内存增长很快,则设置的低一些,防止 Full GC,反之,则可以设置的高一些,尽量减少Old GC。
  3. 由于 CMS 基于标记清除算法,肯定会有内存碎片,因此虚拟机提供了 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 CMS 顶不住要进行 FGC 的最后进行碎片整理,但停顿时间会变长,因此,虚拟机还提供了一个参数 -XX:CMSFullGCsBeforeCompaction ,这个参数是用于设置执行了多少次不压缩的 FGC 后,跟着来一次整理的(默认是0,也就是每次都整理)。

5. G1 收集器(Garbage-First,JDK 9 的默认 GC)

G1 远比 CMS 复杂。

G1 收集器是 Java9 的默认收集器,Oracle 声称 G1 将会替代 CMS。为什么叫 G1 呢,G1 全称 Garbage-First,也就是垃圾优先。这和他的回收策略有关。我们慢慢往下看。

G1有5个特点:

  1. 并行性,G1在回收期间,可以让多个线程同时工作,这点其实上述几个收集器都可以(除了 Serial)。
  2. 并发性,G1 拥有和CMS相同的作用,也就是和应用程序部分并发执行。
  3. 分代 GC,G1 最大的区别就是他既工作在年轻代和工作在老年代,和之前的 GC 收集器完全不同。
  4. 空间整理,我们上面说 CMS 有一个缺点是内存碎片,虽然可以通过一些参数解决,但还是不够完美,而 G1 从某种角度看不是基于标记清除算法,而是基于复制算法。因此不会产生碎片。
  5. 可预测的停顿,由于分区的原因,G1可以只选取部分区域进行内存回收,缩小了范围,相应的减少了系统停顿。

那么 G1 到底是怎么做到这些的呢?

在 G1之前,垃圾收集器的工作范围都是整个新生代或者老年代,但它不是。它的内存布局和其他收集器不同,它将整个 Java 堆分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分 Region 的集合。

G1 只所以可以预测停顿时间,是因为它不再像别的收集器那样收集整个新生代或者老年代,而是回收一部分 Region。

G1 跟踪各个 Region 里面的垃圾的价值大小(回收所获得空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也是 Garbage-First的由来)。这种根据垃圾价值来回收 Region 的方式保证了 G1在有限的时间里回收更多的内存。

这其实就是“化整为零”。

在 JVM 启动时不需要立即指定哪些 Region 属于年轻代,哪些 Region 属于老年代,因为无论是年轻代还是老年代,他们都不需要一大块连续的内存,只是由一系列 Region 组成而已。随着时间的流逝,Region 有时属于新生代,有时属于老年代,来回变动。例如开始的时候,Region A 被分配给年轻代,一个年轻代回收结束后,这个 Region 又被放回了空闲/可用Region 队列,可能下一次就被分配给了一个老年代对象使用。

但是一切并不是那么容易。

Region 不可能是孤立的,一个对象分配在某个 Region 中,他并非只能被本 Region 中的其他对象引用,而是可以与整个 Java 堆任意的对象发生引用关系。在做可达性判断的时候,难道要扫描整个堆吗?也就是说,如果回收新生代的时候同时也扫描老年代,那么 YGC 的效率将会大打折扣。

G1 如何处理这个问题的呢?Region 之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,JVM 都是使用 Remembered Set 来避免全堆扫描的。G1 中每个 Region 都有一个与之对应的 Remembered Set。当Region 中的引用发生变化的时候,G1 将会把这些信息记录到一个 CardTable 数据结构中并存到被引用对象所属Region 的 RSet 中。当进行内存回收哦时,在 GC 根节点的枚举范围中加入 RSet 即可保证不对全堆扫描也不会有遗漏。一般来说,RSet 的大小占整个 Java 堆空间的1% - 20%。

G1 把整个 Java 堆划分成若干个Region,每个 Region 大小为2的倍数,范围在 1MB-32MB 之间,可能是1MB,2MB,4MB,8MB,16MB,32MB。所有的 Region 有一样的大小,最多可以有 2048 个 Region,在 JVM 生命周期内都不会改变。

G1 的收集过程

G1 收集过程分为4个阶段:

  1. 新生代 GC。
  2. 并发标记周期
  3. 混合收集
  4. 如果需要,将进行 FGC

G1 YGC 的过程和之前的YGC 基本相同:当 Eden 区占满,YGC 就会启动,YGC 只处理 Eden 和 Survivor 区,回收后,所有的 Eden 区都应该被清空,而 Survivor 区会被收集一部分数据,但是应用至少仍然存在一个 Survivor 区。另外,老年代的 Region 会增多,因为通常YGC 后会有大量的对象晋升到老年代。

当老年代的使用率达到了一定的阈值,则会触发并发标记,而并发标记的主要目的则是为了标记出那些垃圾比例较高的 Region,为后面的混合收集服务,即收集整个新生代和部分老年代。而并发标记的过程和 CMS 相似。可以参考 CMS 的过程。

在之前的并发标记过程中,已经标记出来垃圾比例较高的 Region,此时轮到混合回收出场了,而这也是 G1 的由来,Garbage First ,优先回收垃圾比例较高的 Region。之所以叫混合回收,是因为既执行正常的年轻代 GC,又会选取一些被标记的老年代 Region 进行回收。被清理的区域中的存货对象会被拷贝到其他区域,消除了 CMS 产生的内存碎片。
混合 GC 会执行多次,直到回收了足够多的内存空间,然后,他会触发一次 YGC,YGC 后,又可能会发生一次并发周期的处理,最后,又会引起混合 GC 的执行,循环反复。如图所示:

混合 GC 过程

如果内存增长的很快,而混合 GC 的速度又跟不上,老年代被填满,则进行一次FGC。而 G1 和 FGC 算法是单线程的 Serial GC,因此会造成长时间的停顿,所以,一定要避免 FGC 出现。

什么时候使用 G1?
如果一个应用程序员具有如下特征,那么将 CMS 或 ParallelOldGC 切换到G1将会大大提高性能。否则还请继续使用 CMS。

  1. Full GC 次数太频繁或者消耗时间太长。
  2. 对象分配的频率或代数(promotion)显著变化。
  3. 受够了太长的垃圾回收或内存整理时间(超过0.5s-1s)。

总结

到这里,我们的5个垃圾收集器大致也就介绍完了。注意,我们这里只是一些 概念性的介绍,甚至没有贴出 GC 日志和大家一起分析。但 GC 的调优是一门艺术,需要不断的试错,才能针对当前的应用找到一个完美的配置,什么是完美的配置?YGC 时间尽量短,FGC 尽量没有,如果有 CMS 或者 G1,尽量保证停顿时间尽可能的短。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值