Java中的垃圾回收器

2023-02-11 08:06:34

一、垃圾回收器

前面我们介绍的所有回收算法都是为实现垃圾回收器服务的,而垃圾回收器就是内存回收的具体实现。目前HotSpot虚拟机用到的垃圾回收器如下图所示。注意只有两个回收器之间有连线才能配合使用。

在这里插入图片描述

1、串行垃圾回收器

在JDK1.3.1之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU或一个收集线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其他所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它简单高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。Client应用或者命令行程序可以,通过-XX:+UseSerialGC可以开启上述回收模式。下图是其运行过程示意图。
在这里插入图片描述

2、并行垃圾回收器

整体来说,并行垃圾回收相对于串行,是通过多线程运行垃圾收集的。也会stop-the-world。适合Server模式以及多CPU环境。一般会和jdk1.5之后出现的CMS搭配使用。并行的垃圾回收器有以下几种: ParNew:Serial收集器的多线程版本,默认开启的收集线程数和cpu数量一样,运行数量可以通过修改ParallelGCThreads设定。用于新生代收集,复制算法。使用-XX:+UseParNewGC,和Serial Old收集器组合进行内存回收。如下图所示。
在这里插入图片描述

Parallel Scavenge: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用cpu时间,尽快完成程序的运算任务可以设置最大停顿时间MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。如果设置了-XX:+UseAdaptiveSizePolicy参数,则随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量。用于新生代收集,复制算法。通过-XX:+UseParallelGC参数,Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。 Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6开始提供的。在此之前Parallel Scavenge的地位也很尴尬,而有了Parllel Old之后,通过-XX:+UseParallelOldGC参数使用Parallel Scavenge + Parallel Old器组合进行内存回收,如下图所示。
在这里插入图片描述

3、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能直到其是给予标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
初始标记:标记一下GC Roots能直接关联到的对象,会“Stop The World”。
并发标记:GC Roots Tracing,可以和用户线程并发执行。
重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
并发清除:清除对象,可以和用户线程并发执行。

在这里插入图片描述

由于垃圾回收线程可以和用户线程同时运行,也就是说它是并发的,那么它会对CPU的资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,并发回收是垃圾收集线程就不会少于25%,而且随着CPU减少而增加,这样会影响用户线程的执行。而且由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。

二、G1垃圾收集器

把G1单独拿出来的原因是其比较复杂,在JDK 1.7确立是项目目标,在JDK 7u2版本之后发布,并在JDK 9中成为了默认的垃圾回收器。通过“-XX:+UseG1GC”启动参数即可指定使用G1 GC。

G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。这样就意味者它空间整合做的比较好,因为不会产生空间碎片。G1还是并发与并行的,它能够充分利用多CPU、多核的硬件环境来缩短“stop the world”的时间。G1还是分代收集的,但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的ygc,全堆扫描的full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC。G1还可预测停顿,通过调整参数,制定垃圾收集的最大停顿时间。

G1收集器的运作大致可以分为以下步骤:初始标记、并发标记、最终标记、筛选回收。其中初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要STW,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行。最终标记阶段则是为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记需要把Remembered Set Logs的数据合并到Remembered Sets中,这阶段需要暂停线程,但是可并行执行。最后的筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。G1收集器运行示意图如下图所示。
在这里插入图片描述

1、G1分区的概念

   G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region,以下和”分区”代表同一概念。(这些分区不要求是连续的内存空间)Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。 JVM的会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约2000个Region。分区大小一旦设置,则启动之后不会再变化。如下图简单画了下G1分区模型。

在这里插入图片描述
Eden regions(年轻代-Eden区)

Survivor regions(年轻代-Survivor区)

Old regions(老年代)

Humongous regions(巨型对象区域)

Free regions(未分配区域,也会叫做可用分区)-上图中空白的区域

G1中的巨型对象是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。ygc也会在某些情况下对巨型对象进行回收。

分区可以有效利用内存空间,因为收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。

2、G1 GC的分类和过程

    JDK10 之前的G1中的GC只有Young GC,Mixed GC。Full GC处理会交给单线程的Serial Old垃圾收集器。

Young GC年轻代收集
在分配一般对象(非巨型对象)时,当所有Eden region使用达到最大阀值并且无法申请足够内存时,会触发一次Young GC。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为Young GC会进行根扫描,所以会stop the world。

Young GC的回收过程如下:

1、根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。

2、处理Dirty card,更新RSet.

3、扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。

4、拷贝扫描出的存活的对象到survivor2/old区

5、处理引用队列,软引用,弱引用,虚引用

Mix GC混合收集
Mixed GC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能够放到CSet里面,有很多参数可以控制。比如G1HeapWastePercent参数,在一次young GC之后,可以允许的堆垃圾百占比,超过这个值就会触发mixed GC。

G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,这个old分区会被放入CSet。

Mixed GC一般会发生在一次Young GC后面,为了提高效率,Mixed GC会复用Young GC的全局的根扫描结果,因为这个Stop the world过程是必须的,整体上来说缩短了暂停时间。

Mix GC的回收过程可以理解为Young GC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象标记,过程如下:

  1. 初始标记(Initial Mark)。标记GC Roots,会STW,一般会复用Young GC的暂停时间。如前文所述,初始标记会设置好所有分区的NTAMS值。

  2. 根分区扫描(Root Region Scan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前Young GC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(Root Region)。

  3. 并发标记(Concurrent Mark)。会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。

  4. 最终标记(Remark)。主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW,可以参考上文的SATB处理。

  5. 清除阶段(Clean UP)。上述SATB也提到了,会进行bitmap的swap,以及PTAMS,NTAMS互换。整理堆分区,调整相应的RSet(比如如果其中记录的Card中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。

清除阶段之后,还会对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。

Full GC
G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC使用的是stop the world的单线程的Serial Old模式,所以一旦触发Full GC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full GC的处理的。对于G1 GC的优化,很大的目标就是没有Full GC。

参考文献:
https://blog.csdn.net/lijingyao8206/article/details/80513383

https://blog.csdn.net/coderlius/article/details/79272773

https://blog.csdn.net/iva_brother/article/details/87886525

《深入理解Java虚拟机》

《垃圾回收的算法与实现》

  • 作者:周康文Kelvin
  • 原文链接:https://blog.csdn.net/m0_49580032/article/details/108264035
    更新时间:2023-02-11 08:06:34