简单概述七种垃圾回收器

• Serial收集器:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。
• ParNew收集器:Serial收集器的多线程版本,也需要stop the world,复制算法。
• Parallel Scavenge收集器:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量。如果虚拟机总共运行100分钟,其中垃圾花掉1分钟,吞吐量就是99%。
• Serial Old收集器:是Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
• Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
• CMS(Concurrent Mark Sweep) 收集器:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片。
• G1收集器:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选标记。不会产生空间碎片,可以精确地控制停顿。

什么是垃圾回收器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。

在JVM中间一般来说垃圾回收器不单单是一个算法,也就是说在JVM垃圾回收器中可能多种算法都有用到

垃圾回收器汇总

新生代垃圾回收器汇总


七种垃圾回收器 - 图1

新生代垃圾回收器汇总

所有的新生代的垃圾回收器都是复制算法

收集器 收集对象和算法 收集器类型
Serial 新生代,复制算法 单线程
ParNew 新生代,复制算法 并行的多线程收集器
Parallel Scavenge 新生代,复制算法 并行的多线程收集器

老年代垃圾回收器汇总

老年代会有两种算法,标记整理算法 和 标记清除算法

收集器 收集对象和算法 收集器类型
Serial Old 老年代,标记整理算法 单线程
Parallel Old 老年代,标记整理算法 并行的多线程收集器
CMS 老年代,标记清除算法 并行与并发收集器
G1 跨新生代和老年代;标记整理 + 化整为零 并行与并发收集器


Serial/Serial Old(串行收集器)



最古老的,单线程串行垃圾回收器,使用复制算法进行垃圾回收,独占式,GC时需要暂停所有用户线程,直到GC完成,但是比较成熟,适合单CPU 服务器

-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old



Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

新生代采用复制算法,老年代采用标记-整理算法.

虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

但是 Serial 收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
七种垃圾回收器 - 图2


ParNew (并行收集器)

和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少

ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样

-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。


ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。

新生代采用复制算法,老年代采用标记-整理算法。
七种垃圾回收器 - 图3

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

并行和并发概念补充:

并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。


Parallel Scavenge(ParallerGC)/Parallel Old (并行收集器)

Parallel Scavenge翻译过来是并行清除的意思

Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。 那么它有什么特别之处呢?

Parallel Scavenge垃圾收集器(类似于ParNew,但是该收集器关注的是cpu的吞吐量,通过参数来控制吞吐量,是吞吐量优先的收集器,同样使用复制算法进行垃圾回收,GC过程需要暂停所有用户线程。)

采用的是复制算法,回收的是新生代

特点:
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那有吞吐效率就是99%。

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

新生代采用复制算法,老年代采用标记-整理算法。
七种垃圾回收器 - 图4

CMS(Concurrent Mark Sweep)

CMS只会收集老年代的内容,只是对单个的区域进行回收,.CMS 在 JDK1.7 之前可以说是最主流的垃圾回收算法。CMS 使用标记清除算法,优点是并发收集,停顿小。

并行收集: CMS首先是多线程的,
并发收集: 同时垃圾收集的多线程和应用的多线程同时进行.

优缺点


优点: 并发收集、低停顿

CMS寻求的是最短的暂停时间为目的的一个收集器.用户线程只有在初始标记和重新标记的时候才会暂停,并且占据的时间非常的短,

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

很多Java程序用CMS还是非常多的,因为CMS垃圾回收器非常的关注响应速度.

缺点:

对CPU的资源是要求比较高的,CPU资源敏感:因为并发阶段多线程占据CPU资源,如果CPU资源不足,效率会明显降低。

浮动垃圾
在进行并发的标记后,开始并发的清理的时候,此时是和用户线程一起运行的,
在执行并发清理的时候,其它用户线程还会有可能产生垃圾.这个时候这次的回收是无法回收这些后来产生的垃圾的,必须等待下一次进行垃圾回收的时候才能清理此时用户产生的垃圾.

由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。(其它垃圾回收器是启动的时候就暂停用户线程,所以不会出现浮动垃圾的情况)

在1.6的版本中老年代空间使用率阈值(92%),就会开启垃圾回收,而不是达到100%的时候才会开启垃圾回收
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

会产生空间碎片:
标记 - 清除算法会导致产生不连续的空间碎片

垃圾回收过程

image.png

阶段1:初始标记,只标记GC root直接引用的对象,这个阶段需要Stop The World,但是影响不大,这个过程特别快。这个过程也可以优化,JVM 有个参数是初始标记阶段多线程标记,减少Stop The World时间,正常是单线程标记的。

阶段2:并发标记,并发标记的线程可以和用户线程一起运行,不需要Stop The World,从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。

阶段3: 重新标记阶段,会把并发标记阶段有改动的对象重新标记,这一步需要Stop The World,不过也是比较快的,因为改动的对象不会特别多,但是要比第一步慢因为要重新判断找个对象是否GC可达。

为什么要重新标记?因为并发标记线程是根着用户线程一起运行的,可能用户线程运行的时候又产生了垃圾,原来标记的垃圾就是脏数据了,所以重新标记是把并发标记和用户线程一起运行的时候,用户线程产生的垃圾再次的进行标记,重新标记就要暂停所有的线程了,为了跑快一点,就得开启多个线程去执行重新标记的操作.

阶段4:并发清理阶段,重新标记完了以后就需要清理了,并发清理是和系统并行的,不需要Stop The World。这个阶段是清理前几个阶段标记好的垃圾。

最后一个阶段是并发重置阶段,为下一次 GC 重置相关数据结构。

Stop The World现象


CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停。

内外的设置正常收集周期是这样的:

1)CMS-initial-mark 初始标记(会stop-the-world)
  2)CMS-concurrent-mark 并发标记的
  3)CMS-concurrent-preclean 执行预清理 注: 相当于两次 concurrent-mark. 因为上一次c mark,太长.会有很多 changed object 出现.先干掉这波.到最好的 stop the world 的 remark 阶段,changed object 会少很多.
  4)CMS-concurrent-abortable-preclean 执行可中止预清理
  5)CMS-remark 重新标记(会stop-the-world)
  6)CMS-concurrent-sweep 并发清除
  7)CMS-concurrent-reset 并发重设状态等待下次CMS的触发

为什么 CMS两次标记时要 stop the world?

我们知道垃圾回收首先是要经过标记的。对象被标记后就会根据不同的区域采用不同的收集方法。看上去很完美的一件事情,其实并不然。
  大家有没有想过一件事情,当虚拟机完成两次标记后,便确认了可以回收的对象。但是,垃圾回收并不会阻塞我们程序的线程,他是与当前程序并发执行的。所以问题就出在这里,当GC线程标记好了一个对象的时候,此时我们程序的线程又将该对象重新加入了“关系网”中,当执行二次标记的时候,该对象也没有重写finalize()方法,因此回收的时候就会回收这个不该回收的对象。
  虚拟机的解决方法就是在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(Stop The World 所以叫STW),暂停后再找到“GC Roots”进行关系的组建,进而执行标记和清除。
  这些特定的指令位置主要在:

1、循环的末尾
2、方法临返回前 / 调用方法的call指令后
3、可能抛异常的位置


G1垃圾回收器

G1 在 1.9 版本后成为 JVM 的默认垃圾回收算法,G1 的特点是保持高回收率的同时,减少停顿。
是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器
其内存结构是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法。

JVM 会尽量划分 2048 个左右、同等大小的 region。数值是在 1M 到 32M 字节之间的一个 2 的幂值数,

在新生代,G1 采用的是并行的复制算法(所以同样会发生 Stop-The-World 的暂停。)
老年代使用的 mixed gc,会回收整个新生代,还会回收一部分的 old region

G1 算法取消了堆中年轻代与老年代的物理划分,但它仍然属于分代收集器。G1 算法将堆划分为若干个区域,称作 Region,如下图中的小方格所示。一部分区域用作年轻代,一部分用作老年代,另外还有一种专门用来存储巨型对象的分区。

image.png
G1 也和 CMS 一样会遍历全部的对象,然后标记对象引用情况,在清除对象后会对区域进行复制移动整合碎片空间。

G1 回收过程如下:
1.G1 的年轻代回收,采用复制算法,并行进行收集,收集过程会 STW。
2.G1 的老年代回收时也同时会对年轻代进行回收。主要分为四个阶段:
3.依然是初始标记阶段完成对根对象的标记,这个过程是STW的;
4.并发标记阶段,这个阶段是和用户线程并行执行的;
5.最终标记阶段,完成三色标记周期;
6.复制/清除阶段,这个阶段会优先对可回收空间较大的 Region 进行回收,即 garbage first,这也是 G1 名称的由来。

G1 采用每次只清理一部分而不是全部的 Region 的增量式清理,由此来保证每次 GC 停顿时间不会过长。
总结如下,G1 是逻辑分代不是物理划分,需要知道回收的过程和停顿的阶段。此外还需要知道,G1 算法允许通过 JVM 参数设置 Region 的大小,范围是 1~32MB,可以设置期望的最大 GC 停顿时间等。有兴趣读者也可以对 CMS 和 G1 使用的三色标记算法做简单了解。


G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数”-XX:G1HeapRegionSize”手动指定Region大小,但是推荐默认的计算方式。
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统
运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前
一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的
Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且
一个大对象如果太大,可能会横跨多个Region来存放。

Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收.

使用的算法:
标记—整理 (old,humongous) 和复制回收算法(survivor)。

七种垃圾回收器 - 图7
新生代的 Eden Survivor
老年代的Old

Humongous 是放大一些的东西

G1收集器特点


• 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
• 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
• 空间整合:与 CMS 的“标记—清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
• 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

  1. 空间整合,不会产生内存碎片(和G1垃圾回收器使用的算法有关)
    2. 可预测的停顿(如果你的垃圾回收器是G1 的话,jvm虚拟机是可以预测处理的你的垃圾回收会停顿多久,可以设置参数,期望停顿的时间是多少,比如期望每一次垃圾回收的时间最长暂停不能超过50毫秒,G1垃圾回收器在垃圾回收的话,就会尽可能的控制在50毫秒之内,但是不一定能做到.)

    内部布局改变

    七种垃圾回收器 - 图8

    G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

    假如说堆内存是8G,G1垃圾回收器把这个容量划分成1000份, 那么每1份大约就是8M的样子,1000份分别有EdenSurvivor Old Humongous 四个区域,这就是化整为零的思想.这样新生代和老年代不再物理隔离。

    当然堆内存设置有最小设置,如果基数小的话,化整为零的意义就不大了.就好比你想弄Docker,如果你服务器内存就只有两三个G了,那么做虚拟机的意义就不大了,除非你机器有100多个G,那么就可以化整为零,划成5个虚拟机,划成10个虚拟机都行,如果你机器只有两三个G,那你用Docker是没有太多意义的.

GC模式

Young GC

每一个区域,这个区域可能就代表一个,如果堆有8G,可能垃圾回收器就会划分成1000个区,Young就会对Eden和Survivor区域里面的东西进行对应的回收,使用复制回收算法.

Mixed GC

这个模式的回收,基本就是采取下图的做法
七种垃圾回收器 - 图9


它主要回收的区域,它就要对这些区域进行标记,它除了对Young选定的所以的Eden和Survivor,这种回收你可以把它想象成类似于所谓的 Full GC,但是和Full GC 有点区别.
首先Mixed GC可以回收Eden,同样还会对Old区域对应的标记,所谓的标记就是全局并发标记

首先会先初始标记,初始标记和CMS的标记其实是差不多的,然后做完以后再做并发标记,并发标记做完以后再做最终标记,然后再来做所谓的回收.

七种垃圾回收器 - 图10
上面的四步回收过程(初始标记,并发标记.最终标记,回收),可能会针对堆里面划分好的编号为1024(编号是随机的)独立区域(Region),也可能是编号为9528的独立区域,可能这时候G1线程可能只对1024编号的独立区域进行回收,因为会有优先的顺序
也就是说,如果我采用G1的垃圾回收器,它为什么能够做到可预测的停顿?

G1垃圾回收器会做一个智能的判断,它会进行优化,因为对堆进行划分了区域,所以说可以对优先需要回收的区域进行垃圾回收.

假如说有1000个区域,一个区域回收的时间是10毫秒,这个时候如果我时间卡1秒之内的话,我就只是回收前100个需要优先进行垃圾回收的区域进行垃圾回收.这个时间回收的停顿时间就可以预测了. 当然,任何的垃圾回收器都会有停顿的,没有说不需要停顿的,只是停顿时间的长和短而已.

所以化整为零的好处就是可以在有限的时间内可以获取最高的回收效率.

可预测的停顿:
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1把内存“化整为零”的思路

全局并发标记(global concurrent marking)


初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。

并发标记:从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。

最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。

筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

七种垃圾回收器 - 图11



垃圾回收器的重要参数(使用-XX:)

参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加1,当超过这个参数值时就进入老年代
UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为99,即允许 1% 的GC时间,仅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效











简单的垃圾回收器工作示意图


七种垃圾回收器 - 图12

单线程收集就是新生代是一个线程收集,老年代是一个线程收集.
使用并行收集的话,多个线程可以同时启动垃圾回收器
简单的垃圾回收器在每进行一次回收的时候,它会暂停所有的用户线程.等待所有的垃圾回收器完成工作,用户线程才能继续工作.
这就是为什么不能在代码里面写System.GC()的原因了,因为进行垃圾回收的时候所有的用户线程都会暂停.