你知道在JVM有哪些垃圾回收器吗?
我们介绍了 Java 虚拟机的内存结构,Java 虚拟机的垃圾回收机制,那么这篇文章我们说说具体执行垃圾回收的垃圾回收器。
总的来说,Java 虚拟机的垃圾回收器可以分为四大类别:串行回收器、并行回收器、CMS 回收器、G1 回收器。
串行回收器
串行回收器是指使用单线程进行垃圾回收的回收器。因为每次回收时只有一个线程,因此串行回收器在并发能力较弱的计算机上,其专注性和独占性的特点往往能让其有更好的性能表现。
串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器。
新生代串行回收器
串行收集器是所有垃圾回收器中最古老的一种,也是 JDK 中最基本的垃圾回收器之一。
在新生代串行回收器中使用的是复制算法
。在串行回收器进行垃圾回收时,会触发 Stop-The-World 现象,即其他线程都需要暂停,等待垃圾回收完成。因此在某些情况下,其会造成较为糟糕的用户体验。
使用 -XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。当虚拟机在 Client 模式下运行时,其默认使用该垃圾收集器。
老年代串行回收器
在老年代串行回收器中使用的是标记压缩算法。其与新生代串行收集器一样,只能串行、独占式地进行垃圾回收,因此也经常会有较长时间的 Stop-The-World 发生。
但老年代串行回收器的好处之一,就是其可以与多种新生代回收器配合使用。若要启用老年代串行回收器,可以尝试以下参数:
- -XX:UseSerialGC:新生代、老年代都使用串行回收器。
- -XX:UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。
- -XX:UseParallelGC:新生代使用 ParallelGC 回收器,老年代使用串行回收器。
并行回收器
并行回收器在串行回收器的基础上做了改进,其使用多线程进行垃圾回收。对于并行能力强的机器,可以有效缩短垃圾回收所使用的时间。
根据作用内存区域的不同,并行回收器也有三个不同的回收器:新生代 ParNew 回收器、新生代 ParallelGC 回收器、老年代 ParallelGC 回收器。
新生代 ParNew 回收器
新生代 ParNew 回收器工作在新生代,其只是简单地将串行回收器多线程化,其回收策略、算法以及参数和新生代串行回收器一样。
新生代 ParNew 回收器同样使用复制的垃圾回收算法,其垃圾收集过程中同样会触发 Stop-The-World 现象。但因为其使用多线程进行垃圾回收,因此在并发能力强的 CPU 上,其产生的停顿时间要短于串行回收器。
但在单 CPU 或并能能力弱的系统中,并行回收器效果会因为线程切换的原因,其实际表现反而不如串行回收器。要开启新生代 ParNew 回收器,可以使用以下参数:
- -XX:+UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器。
- -XX:UseConcMarkSweepGC:新生代使用 ParNew 回收器,老年代使用 CMS。
- -XX:ParallelGCThreads:指定 ParNew 回收器的工作线程数量。
新生代 Parallel GC 回收器
新生代 Parallel GC 回收器与新生代 ParNew 回收器非常类似,其也是使用复制算法,都是多线程、独占式的收集器,也会导致 Stop-The-World。但其余 ParNew 回收器的一个重大不同是:其非常注重系统的吞吐量。
之所以说新生代 Parallel GC 回收器非常注重系统吞吐量,是因为其有一个自适应 GC 调节策略。我们可以使用 -XX:+UseAdaptiveSizePolicy 参数打开这个策略,在这个模式下,新生代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数都会被自动调节,已达到堆大小、吞吐量、停顿时间的平衡点。
Parallel GC 回收器提供了两个重要参数用于控制系统的吞吐量。
- -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间。在 ParallelGC 工作时,其会自动调整响应参数,将停顿时间控制在设置范围内。为了达到目的,其可能会使用较小的堆,但这会导致 GC 较为频繁。
- -XX:GCTimeRatio:设置吞吐量大小,其实一个 0 - 100 的整数。假设 GCTimeRatio 的值为 n,那么系统将不花费超过 1/(1+n) 的时间用于垃圾手机。比如 GCTimeRatio 值为 19,那么系统用于垃圾收集的时间不超过 1 /(1+19) = 5%。默认情况下,它的取值是 99,即不超过 1% 的时间用于垃圾收集。
新生代 Parallel GC 回收器可以使用以下参数启用:
- -XX:+UseParallelGC:新生代使用 Parallel 回收器,老年代使用串行回收器。
- -XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。
老年代 ParallelOldGC 回收器
老年代 ParallelOldGC 回收器也是一种多线程并发的回收器,与新生代 ParallelGC 收集器一样,其也是注重吞吐量的收集器,只不过其是作用于老年代。
ParallelOldGC 回收器使用的是标记压缩算法,只有在 JDK 1.6 中才可以使用。我们可以使用
-XX:UseParallelOldGC参数在新生代中使用 ParallelGC 收集器,在老年代中使用 ParallelOldGC 收集器。
参数 -XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。
CMS 回收器
与 ParallelGC 和 ParallelOldGC 不同,CMS 回收器主要关注系统停顿时间。CMS 回收器全称为 Concurrent Mark Sweep,意为标记清除算法,其是一个使用多线程并行回收的垃圾回收器。
工作步骤
CMS 的主要工作步骤有:初始标记、并发标记、预清理、重新标记、并发清除和并发充值。其中初始标记和重新标记是独占系统资源的,而其他阶段则可以和用户线程一起执行。
在整个 CMS 回收过程中,默认情况下会有预清理的操作,我们可以关闭开关 -XX:-CMSPrecleaningEnabled 不进行预清理。因为重新标记是独占 CPU 的,因此如果新生代 GC 发生之后,立刻出发一次新生代 GC,那么停顿时间就会很长。为了避免这种情况,预处理时会刻意等待一次新生代 GC 的发生,之后在进行预处理。
主要参数
启动 CMS 回收器刻意使用参数:-XX:+UseConcMarkSweepGC,线程并发数量刻意通过 -XX:ConcGCThreads 或 -XX:ParallelCMSThreads 参数设定。
此外,我们还可以设置 -XX:CMSInitiatingOccupancyFraction 来指定老年代空间使用阈值。当老年代空间使用率达到这个阈值时,会执行一次 CMS 回收,而不像其他回收器一样等到内存不够用的时候才进行 GC。
我们之前说过标记清除算法的缺点是会产生内存碎片,因此 CMS 回收器会产生较多内存碎片。我们可以使用 XX:+UseCMSCompactAtFullCollection 参数让 CMS 在完成垃圾回收后,进行一次内存碎片整理。使用 -XX:CMSFullGCsBeforeCompaction 参数设置进行多少次 CMS 回收后,进行一次内存压缩。
此外,如果希望使用 CMS 回收 Perm 区,那么则可以打开 -XX:+CMSClassUnloadingEnabled 开关。打开该开关后,如果条件允许,那么系统会使用 CMS 的机制回收 Perm 区 Class 数据。
G1 回收器
G1 回收器是 JDK 1.7 中使用的全新垃圾回收器,从长期目标来看,其是为了取代 CMS 回收器。
G1 回收器拥有独特的垃圾回收策略,和之前所有垃圾回收器采用的垃圾回收策略不同。从分代看,G1 依然属于分代垃圾回收器。但它最大的改变是使用了分区算法,从而使得 Eden 区、From 区、Survivor 区和老年代等各块内存不必连续。
在 G1 回收器之前,所有的垃圾回收器其内存分配都是连续的一块内存,如下图所示。
而在 G1 回收器中,其将一大块的内存分为许多细小的区块,从而不要求内存是连续的。
从上图可以看到,每个Region被标记了 E、S、O 和 H,说明每个 Region 在运行时都充当了一种角色。所有标记为 E 的都是 Eden 区的内存,它们散落在内存的各个角落,并不要求内存连续。同理,Survivor 区、老年代(Old)也是如此。
从上图我们还可以看到 H 是以往算法中没有的,它代表 Humongous。这表示这些 Region 存储的是巨型对象(humongous object,H-obj),当新建对象大小超过 Region 大小一半时,直接在新的一个或多个连续 Region 中分配,并标记为 H。
堆内存中一个 Region 的大小可以通过 -XX:G1HeapRegionSize 参数指定,大小区间只能是1M、2M、4M、8M、16M 和 32M,总之是2的幂次方。如果G1HeapRegionSize 为默认值,即把设置的最小堆内存按照2048份均分,最后得到一个合理的大小。
工作步骤
G1 收集器的收集过程主要有四个阶段:
- 新生代 GC
- 并发标记周期
- 混合收集
- 如果需要,可能进行 FullGC
新生代 GC 与其他垃圾收集器的类似,就是清空 Eden 区,将存活对象移动到 Survivor 区,部分年龄到了就移动到老年代。
并发标记周期则分为:初始标记、根区域扫描、并发标记、重新标记、独占清理、并发清理阶段。其中初始标记、重新标记、独占清理是独占式的,会引起停顿。并且初始标记会引发一次新生代 GC。在这个阶段,所有将要被回收的区域会被 G1 记录在一个称之为 Collection Set 的集合中。
混合回收阶段会首先针对 Collection Set 中的内存进行回收,因为这些垃圾比例较高。G1 回收器的名字 Garbage First 就是这个意思,垃圾优先处理的意思。在混合回收的时候,也会执行多次新生代 GC 和 混合 GC,从而来进行内存的回收。
必要时进行 Full GC。当在回收阶段遇到内存不足时,G1 会停止垃圾回收并进行一次 Full GC,从而腾出更多空间进行垃圾回收。
相关参数
打开 G1 收集器,我们可以使用参数:`-XX:+UseG1GC。
设置目标最大停顿时间,可以使用参数:-XX:MaxGCPauseMillis。
设置 GC 工作线程数量,可以使用参数:-XX:ParallelGCThreads。
设置堆使用率触发并发标记周期的执行,可以使用参数:-XX:InitiatingHeapOccupancyPercent。
从一开始的串行回收器,到后来的并行回收器、CMS回收器,到最后的 G1 回收器,垃圾回收器不断改进,使得垃圾回收效率不断提升。特别是分区思想诞生后,对于垃圾回收停顿时间的控制更加细腻,可以让应用有更完美的延时控制,从而呈现更好的用户体验。
垃圾回收的几种类型
我们经常会听到许多垃圾回收的术语,例如:Minor GC、Major GC、Young GC、Old GC、Full GC、Stop-The-World 等。但这些 GC 术语到底指的是什么,它们之间的区别到底是什么?今天我们就来详细说说。
Minor GC
从年轻代空间回收内存被称为 Minor GC,有时候也称之为 Young GC。对于 Minor GC,你需要知道的一些点:
- 当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以 Eden 区越小,越频繁执行 Minor GC。
- 当年轻代中的 Eden 区分配满的时候,年轻代中的部分对象会晋升到老年代,所以 Minor GC 后老年代的占用量通常会有所升高。
- 质疑常规的认知,所有的 Minor GC 都会触发 Stop-The-World,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的,因为大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果情况相反,即 Eden 区大部分新生对象不符合 GC 条件(即他们不被垃圾回收器收集),那么 Minor GC 执行时暂停的时间将会长很多(因为他们要JVM要将他们复制到 Survivor 区或老年代)。
Major GC
从老年代空间回收内存被称为 Major GC,有时候也称之为 Old GC。
许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。
Minor GC 作用于年轻代,Major GC 作用于老年代。 分配对象内存时发现内存不够,触发 Minor GC。Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。因此才会说,许多 Major GC 是由 Minor GC 引起的。
Full GC
Full GC 是清理整个堆空间 —— 包括年轻代、老年代和永久代(如果有的话)。因此 Full GC 可以说是 Minor GC 和 Major GC 的结合。
当准备要触发一次 Minor GC 时,如果发现年轻代的剩余空间比以往晋升的空间小,则不会触发 Minor GC 而是转为触发 Full GC。因为JVM此时认为:之前这么大空间的时候已经发生对象晋升了,那现在剩余空间更小了,那么很大概率上也会发生对象晋升。既然如此,那么我就直接帮你把事情给做了吧,直接来一次 Full GC,整理一下老年代和年轻代的空间。
另外,即在永久代分配空间但已经没有足够空间时,也会触发 Full GC。
Stop-The-World
Stop-The-World,中文一般翻译为全世界暂停,是指在进行垃圾回收时因为标记或清理的需要,必须让所有执行任务的线程停止执行任务,从而让垃圾回收线程回收垃圾的时间间隔。
在 Stop-The-World 这段时间里,所有非垃圾回收线程都无法工作,都暂停下来。只有等到垃圾回收线程工作完成才可以继续工作。可以看出,Stop-The-World 时间的长短将关系到应用程序的响应时间,因此在 GC 过程中,Stop-The-World 的时间是一个非常重要的指标。