【jvm系列-10】深入理解jvm垃圾回收器的种类以及内部的执行原理

JVM系列整体栏目


内容 链接地址
【一】初识虚拟机与java虚拟机 https://blog.csdn.net/zhenghuishengq/article/details/129544460
【二】jvm的类加载子系统以及jclasslib的基本使用 https://blog.csdn.net/zhenghuishengq/article/details/129610963
【三】运行时私有区域之虚拟机栈、程序计数器、本地方法栈 https://blog.csdn.net/zhenghuishengq/article/details/129684076
【四】运行时数据区共享区域之堆、逃逸分析 https://blog.csdn.net/zhenghuishengq/article/details/129796509
【五】运行时数据区共享区域之方法区、常量池 https://blog.csdn.net/zhenghuishengq/article/details/129958466
【六】对象实例化、内存布局和访问定位 https://blog.csdn.net/zhenghuishengq/article/details/130057210
【七】执行引擎,解释器、JIT即时编译器 https://blog.csdn.net/zhenghuishengq/article/details/130088553
【八】精通String字符串底层机制 https://blog.csdn.net/zhenghuishengq/article/details/130154453
【九】垃圾回收底层原理和算法以及JProfiler的基本使用 https://blog.csdn.net/zhenghuishengq/article/details/130261481
【十】垃圾回收器的种类以及内部的执行原理 https://blog.csdn.net/zhenghuishengq/article/details/130261481

一,jvm中的垃圾回收器

1,垃圾回收器的概述

在《java虚拟机规范》中,并没有明确的对垃圾收集器做过多的规定,因此垃圾收集器可以是由任意产商,不同版本的JVM来实现。因此从不同角度来分析这个垃圾收集器,就可以将GC垃圾收集器分为不同的类型
在这里插入图片描述

如可以按照执行垃圾线程的线程数量来分类,可以分为串行垃圾回收器和并行垃圾回收器;也可以按照工作模式来区分,可以分为并发式垃圾回收器和独占式垃圾回收器;也可以按照碎片的处理方式区分,可以分为压缩式垃圾回收器和非压缩式垃圾回收器;也可以按照工作的内存区间分,可以分为年轻代回收器和老年代回收器

而在面对一款垃圾回收器的GC评估的时候,主要是从以下的几个方面很做出评价

  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序运行的时间 + 垃圾回收的时间 )
  • 垃圾收集开销:垃圾收集所占总运行时间与总运行时间的比例
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(stw)
  • 收集频率:相对于应用程序的执行,收集操作发生的频率
  • 内存占用:Java堆区所占的内存大小。
  • 周期:一个对象从诞生到被回收所经历的时间

在这几个指标中,吞吐量、暂停时间和内存占用这三个指标又是重中之重。因为这三者每次只能满足其中的二者,吞吐量和这个暂停时间只能二选一

1.1,吞吐量和暂停时间

上面也提到了,这个吞吐量指的是CPU用于运行用户代码的时间与总CPU消耗时间的比值,即运行用户代码的时间/运行用户代码的时间 + 垃圾收集时间,如虚拟机总共运行了100分钟,垃圾收集花掉了1分钟,运行用户代码花掉了99分钟,那么其吞吐量就是 99 / 100 = 99%。

而注重吞吐量,则不需要考虑暂停时间的大小,如下图,在6s内,尽管上面的暂停时间是比较长,但是其暂停时间的占比是比较小的,因此可以认为更加的注重吞吐量,也可以认为注重吞吐量的垃圾收集器可以不用考虑这个stw的暂停时长

在这里插入图片描述

而注重低延迟的垃圾收集器,其吞吐量要低于上面这个,但是其延迟时间小,那么每次stw的时间就会更短,那么需要的空间也可以小一点。因此也可以证明上面的吞吐量和这个暂停时间是互为矛盾的。

高吞吐量的优点在于可以让应用程序始终感觉只有应用程序线程在做生产性工作,从直觉上来看,吞吐量越高程序运行的越快;低延迟的好处在于从整个流程来看,如果是因为某个应用导致被挂起一段时间始终是不友好的,因为200ms的stw暂停也可能会严重的影响到用户的体验,因此具有较低的暂停是很有必要的。因此,如果是一个交互式的应用程序,那么较低的延迟是优先选择,如剑圣一个q下去,你卡我几秒再掉血,那我不得把这游戏恨死。

如果优先选择吞吐量,那么比如会降低回收的执行频率,但是同时也会让stw的时间更长;如果优先选择低延迟,那么回收的频率增高,会导致新生代内存的缩减和程序吞吐量的下降

因此现在的垃圾回收器的标准是:在最大吞吐量优先的情况下,再降低停顿时间

1.2,垃圾回收器的种类以及概述

接下来通过这个垃圾收集器的历史,来详细说明一下垃圾收集器的种类。

  • 1999年,Serial GC横空出世,是第一款GC,以串行的方式运行
  • 2002年,Parallel GC和 CMS GC同时发布,以并行的方式运行,JDK6开始的默认GC
  • 2017年,JDK9中将G1变成默认的垃圾收集器,从而代替CMS
  • 2018年,JDK11发布,并且引入ZGC,可伸缩低延迟垃圾回收器
  • 2019年,JDK12发布,增强了这个G1,自动返回未用堆内存给操作系统
  • 2019年,JDK13发布,增强了ZGC,自动返回未用堆内存给操作系统
  • 2020年,JDK14发布,删除了CMS,CMS成为历史

而垃圾回收器的搭配使用如下,ParNew GC和这个CMS搭配使用,Parallel Scavenge GC和Parallel Old GC搭配使用,这个Serial GC和Serial Old GC搭配使用。在这三个组合中,前者都是用来回收新生代,后者都是用来回收老年代的垃圾,如下面的蓝色部分就是用来回收新生代,橙色部分就是用来回收老年代,而后面引进的G1垃圾回收器,既可以回收新生代,也可以回收老年代。

在这里插入图片描述

通过上面的垃圾收集器,得知有7款经典的垃圾回收器

  • 串行垃圾回收器:Serial、Serial Old
  • 并行回收器:ParNew 、 Parallel Scavenge 、Parallel Old
  • 并发回收器:CMS、 G1

在JDK8中,默认主要还是Parallel Scavenge + Parallel Old组合,或者是CMS + ParNew组合,如果是在C端,能够确认是单线程的环境下运行,那么也可以选择使用Serial + Serial Old组合。Parallel和ParNew的性能不相上下,但是由于框架的不兼容,导致只有这个ParNew可以和这个CMS组合,而这个Parallel不能和这个CMS组合。

查看当前默认的垃圾回收器的指令如下:

-XX:+PrintCommandLineFlags

或者通过命令的形式查看

//查看所有进程
jps
//通过进程号查看对应的信息
jinfo -flag UseParallel 进程号

2,Serial垃圾回收器

该回收器是最基本的,也是历史最悠久的串行垃圾回收器。

如下图详细的描述了Serial中的整个垃圾回收的执行流程。在新生代中,使用的是复制算法,如典型的s0区和s1区,并采用的是串行回收和stw机制的方式执行内存回收;在老年代中使用的是标记整理法,并且使用的是Serial搭配使用的Serial Old收集器,采用的也是是串行回收和stw机制。stw机制一般是在一个SafePoint的安全点的时候触发,并且此时会暂停所有的用户线程,从而执行垃圾回收线程

在这里插入图片描述

Serial Old主要有两个用途,一个是与新生代的Serial结合使用,一个是作为老年代CMS的替补方案。

Serial虽然是串行模式下的回收器,但是相对于其他的收集器而言,也有着其自身的优势。对于限定单个CPU而言,Serial收集器没有线程交互的开销,专心的做垃圾收集因此可以获取到最高的单线程效率。比如在用户桌面应用场景中,可用内存一般不大,可以在较短的时间内完成垃圾收集,只要垃圾收集的频率不要过于频繁,那么可以优先的选择这个串行的垃圾收集器。

在jvm虚拟机中,使用Serial收集器的参数如下

-XX:+UseSerialGC

也可以通过命令行的形式查看

//查看所有进程
jps
//通过进程号查看对应的信息
jinfo -flag UseSerialGC 进程号

而其缺点就是只能限定于单核CPU内运行,很难满足当代开发者们的需求。

3,ParNew垃圾回收器

Serial属于是单线程的垃圾回收器,而ParNew则是Serial的多线程版本,也就是说,ParNew除了可以采用并行回收的特性之外,其本质和Serial回收器无任何区别

ParNew收集器在新生代中也是采用的是并行模式,复制算法,使用到了stw机制,并且在很多的Server模式下,都是作为他们的默认垃圾收集器;但是在老年代中,可以搭配这个并发模式的CMS或者这个串行模式的Serial Old使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K0Za7NGh-1682412834185)(img/1682307386705.png)]

对于新生代而言,其触发的GC次数频率会更高,回收的次数更加频繁,因此使用并行的方式执行,并采用空间换时间的复制算法来提高效率;对于老年代而言,GC的次数会更低,回收的次数更少,因此可以使用串行的方式,并采用更加平滑的标记整理法来节省资源。

在jvm虚拟机中,使用ParNew收集器的参数如下

-XX:+UseParNewGC

也可以通过命令行的形式查看

//查看所有进程
jps
//通过进程号查看对应的信息
jinfo -flag UseParNewGC 进程号

4,Parallel垃圾回收器(高吞吐量)

在HotSpot虚拟机中,Parallel在新生代和ParNew有着相同的特性,都是使用的是并行回收,以空间换时间的效率高的复制算法,以及对应的stw机制,不同的是ParNew仅有并行的特性,Parallel则在此基础上多加了一个 可控制的吞吐量 的特性,也被称为吞吐量优先的垃圾收集器。高吞吐量则可以高效率的利用CPU时间,尽快的完成运算任务,如批量处理,订单处理等。

和老年代中的 Parallel Old 结合使用,Parallel Old使用的算法是标记整理法,但同样也是基于并行回收和stw机制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bM5FwmTB-1682412834186)(img/1682317043639.png)]

在吞吐量优先场景中,Parallel和Parallel Old收集器的组合可以优先考虑,此收集器在Java8中也是默认收集器。

在jvm虚拟机中,使用Parallel收集器的参数如下

-XX:+UseParallelGC          //手动指定新生代
-XX:+UseParallelOldGC       //手动指定老年代
-XX:ParallelGCThreads       //设置新生代并行收集器的数量,大小最好设置和cpu数量相等
-XX:MaxGCPauseMillis        //设置垃圾回收器最大的停顿时间
-XX:GCTimeRatio             //垃圾收集时间占总时间的比例
-XX:+UseAdaptiveSizePolicy  //手动指定老年代

也可以通过命令行的形式查看

//查看所有进程
jps
//通过进程号查看对应的信息
jinfo -flag UseParallelGC  进程号

5,CMS垃圾回收器(低延迟)

CMS:Concurrent-Mark-Sweep,第一款真正意义上的并发收集器,第一次实现了让垃圾收集线程与用户线程同时工作。CMS收集器关注的点是尽可能缩短垃圾收集时用户程序的停顿时间,也就是说,停顿的时间越短,就越适合与用户交互的程序,良好的响应速度可以提升用户体验。此垃圾回收采用的算法是标记清除法

5.1,CMS处理器工作流程(重点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NnmWDDVZ-1682412834186)(img/1682321410352.png)]

通过上图可知,在CMS的工作流程主要分为四个阶段,分别是初始标记、并发标记、重新标记和并发清理

初始标记:在这个阶段中,程序的工作线程会因为stw机制而出现短暂的暂停,这个阶段的任务主要是标记出GC Root可以关联到的对象,一旦标记完成,就会恢复之前的状态。由于直接关联的对象比较小,因此这里的速度非常的块。

并发标记:从GC Roots的直接关联的对象开始遍历整个对象的过程,这个过程较长,但是不需要停顿用户线程,可以让用户线程和垃圾回收线程同时工作。

重新标记:由于在并发标记阶段中,工作线程和垃圾回收线程同时工作,难免会有新的垃圾出现或者旧的垃圾复活(finalize),因此需要做一次重新标记。此时会触发stw,需要消耗的时间稍长,但远比并发标记阶段时间短。

并发清除:此阶段是真正的清理掉标记阶段判断已死的对象,并且释放内存空间

尽管CMS收集器采用的是并发回收,但是其初始化标记和再次标记仍然会触发stw,不过暂停的时间不会太长。由于最费时的并发标记和并发清除都是并发执行,因此用户线程不需要暂停,从而让整体的垃圾回收是低延迟的。

CMS主要是用户回收老年代的对象,而用户线程和垃圾回收线程在同时工作,如果在内存满了再触发这个垃圾回收,那么这个用户线程就会运行不了,因此不能在内存满了的时候才触发垃圾回收工作,而是需要设置一个阈值,在达到阈值的时候就触发这个垃圾回收的工作。

5.2,CMS采用标记清除算法的原因

在目前的三种垃圾算法中,新生代一般使用复制算法,老年代一般使用标记整理法或者标记清除法,由于标记清除会产生大量的垃圾碎片,因此老年代一般优先选择使用标记整理法,那为啥这里会使用标记清除呢?

由于垃圾回收采用的是标记清除算法,因此可能会出现这个碎片化的问题,因此在分配内存的时候只能使用空闲列表的方式,而不能使用指针碰撞。主要是因为并发清理的时候,用户线程也在工作,即使标记整理法可以解决这个碎片化的问题,但是标记整理法会涉及到对象的移动问题,而用户线程正在工作,对象的地址是绝对不能发生改变的,因此这里只能选择标记清除算法,而不能选择标记整理算法。用户线程正在工作,你把人家地址给改变了,人家不得去告你。

如果一定要选择标记整理法,那么需要stw停止用户线程,这里为了低延迟,显然不可能在并发清理阶段出现stw

5.3,CMS回收器的优缺点以及可设置参数

CMS优点:实现了并发收集;低延迟

CMS缺点:采用的标记清除法,会产生垃圾碎片;对CPU资源敏感;并发标记可能出现新的垃圾,因此无法处理这种浮动垃圾

在jvm虚拟机中,使用CMS收集器的参数如下

-XX:+UseConcMarkSweepGC             //手动指定使用CMS收集器
-XX:CMSInitiatingOccupanyFraction   //设置堆内存使用率阈值,达到该阈值时触发垃圾回收
-XX:+UseCMSCompactAtFullColection   //指定FULL GC之后对内存空间进行整理
-XX:CMSFullGcsBeforeCompaction      //设置多少次FULL GC之后对内存空间进行整理
-XX:ParallelCMSThreads              //设置CMS线程的数量

因此通过这些垃圾回收器的优缺点可知,垃圾回收器的选择策略如下:想要最小化的使用内存和并行开销------Serial ,最大化应用程序的吞吐量------Parallel,最小化中断或者停顿时间------CMS

在JDK8中,CMS配合ParNew使用成为默认的垃圾回收器;在JDK14的版本中,CMS直接被移除不再使用,CMS成为历史。

6,G1垃圾处理器(重点)

6.1,G1垃圾回收器概述

从JDK9开始,使用的垃圾回收器就是G1回收器,被称为区域化分代式回收器。虽然已经有了高吞吐量的Parallel,以及低延迟的CMS,但是随着业务越来越大,复杂,以及用户越来越多,这两款处理器显然不能满足实际需求,并且随着内存的不断扩大和处理器的数量不断的增加,为了兼容高吞吐量和低延迟,使二者都可以满足需求,因此G1诞生。官方给G1设定的目标是:在延迟可控的情况下,尽可能的提高吞吐量。

G1是一个并行回收器,将堆内存分割为很多不相关区域,如Eden,s0,s1,老年代等。主动跟踪各个region,在每个region里面计算出其垃圾堆积的价值大小,然后在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。就是说不管区域中总共对象的多少,而是哪个区域中的垃圾多,就优先回收哪个region区域,因此G1名字的由来-----Garbage First(垃圾优先)

6.2,G1回收器的特点

并行与并发:G1有并行性,可以在回收期间,有多个GC同时工作,此时会触发STW;也有并发性,可以与应用程序交替执行,不会触发stw,在整个阶段不会出现完全阻塞的情况。

分代收集:G1仍然属于分代收集器,他依然会去区分新生代和老年代,不同的是,堆中的对象不要求是连续的空间,也不需要固定其大小和数量。如下图,如果出现某个Eden区的所有对象被清除,那么那块地址下一次存储的就不一定是Eden区对象,可能是s区或者old区的对象。而在每个region中,只允许存在一种类型的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZlgvA02-1682412834187)(img/1682389504407.png)]

空间整合:CMS采用的是标记清除算法,那么会产生内存碎片化问题。G1将内存划分为一个个小的region,内存的回收也是以region为单位的,其内部采用的是复制算法,整体可以看做成标记整理法,从而解决内存碎片化的问题,从而解决大对象不会因为无法找到连续内存空间而提前触发一次GC。

可预测的停顿时间模型:G1和CMS都是为了低延迟而诞生,G1除了追求低延迟之外,还可以建立一个可预测的停顿时间模型,能让使用者明确知道在一个长度为M的毫秒时间判断内,消耗在垃圾收集上不得超过N秒

6.3,G1回收器的参数设置

可以设置的参数如下

-XX:+UseG1Gc                    	//手动指定使用G1收集器
-XX:G1HeapRegionSize           		//设置region大小,值为2的幂
-XX:MaxGcPauseMillis                //设置最大GC停顿指标
-XX:ParallelGCThread                //设置stw工作线程的值
-XX:ConcGCThreads                   //设置并发标记的线程数
-XX:InitiatingHeapOccupancyPercent  //设置触发并发GC周期的Java堆占用阈值

G1的调优原则就是简化JVM的性能调优,开发人员只需要简单的三步就可以完成调优:开启G1垃圾收集器,设置最大的堆内存,设置最大的停顿时间

6.4,Region

在G1收集器中,它将整个Java堆划分成2048个独立大小的Region块,每个region块大小根据实际的大小而定,并且大小的值为2的幂次方,可以通过-XX:G1HeapRegionSize 来设置。在region中,不需要新生代和老年代逻辑连续。

如下图,一个region只能属于一个角色,E表示eden区,S表示Survivor区,O表示old区,空白部分表示未使用区域。同时在G1中,增加了一个Humongous内存区域,主要用于存储大对象,如果超过1.5个region,那么该对象存储在Humongous内存区域内,如果一个H区域存储不下,那么就会寻找一个连续的H区存储

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dxld8ypn-1682412834187)(img/1682389504407.png)]

6.5,G1回收器回收垃圾的过程

G1垃圾回收器和上面的垃圾回收器有着不同之处,上面的几款回收器主要是针对于新生代或者老年代的其中一个区域进行垃圾回收,而G1是即要对新生代进行垃圾回收,也要对老年代进行垃圾回收。G1的垃圾回收器主要包括三个环节:年轻代GC,老年代并发标记过程,回合回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E543SGGl-1682412834188)(img/1682393777571.png)]

  • 当新生代的Eden区用尽时,就会开始触发年轻代的垃圾回收过程: G1的新生代收集阶段是一个并行的独占式的垃圾收集器。在新生代的回收期,G1 GC暂停所有的应用程序线程,启动多线程执行新生代回收。然后从新生代区间移动存活对象到s区或者老年代中。
  • 当堆内存达到45%的时候,就会触发老年代并发标记过程
  • 标记完成后开始混合回收的过程: 对于一个混合回收期,G1 GC从老年代区间将对象移动到空闲区间,这些空闲区间就成为了老年代的一部分。老年代回收器不需要将整个老年代回收,一次只需要扫描一部分老年代的区域即可。

6.5,记忆集和写屏障

在每个region中,region与region之间肯定存在相互引用,因此在region中,引入了这个Remember Set,被称为结果记忆集。每个记忆集中记录着引用着当前对象的对象地址,并且在写入记忆集时,被称为写屏障。并且在写入时,会有一个短暂的中断操作,回去判断写入的对象是否和当前类型的数据在不同的region,如果不同,那么就将相关引用的信息加载到Rset中,在进行垃圾回收时,则只需要扫描记忆集中的内容,而不需要进行全盘扫描。

如下图,每个Region区域都有对应的Rset记忆集, 如Region2区域,该区域被region1和region3所引用着,那么在region2区域对应的rset中,就会记录这个region1和region3的相关信息。如果Region2要被判断是否为垃圾并且进行回收的话,则只需通过这个Rset中记录的值即可缩小范围。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1M4ubSDP-1682412834188)(img/1682408274118.png)]

6.6,G1垃圾回收详解

新生代的回收过程如下:

  • 先扫描Root根,如一些静态对象,方法中的局部变量等,作为RSet的入口,
  • 然后再更新RSet,RSet可以准确的反映老年代对所在的内存分段中对象的引用,
  • 随后再处理这个RSet,老年代指向的Eden中的对象即为存活对象,
  • 随后再复制对象,将eden区的对象复制到s区或者old区,最后再去处理引用,如一些强、软、弱、虚引用等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PUQiRiIP-1682412834189)(img/1682409311073.png)]

并发标记的过程如下:(该阶段和CMS阶段很类似)

  • 先是初始标记阶段,标记从根节点直接可达的对象,会触发stw;
  • 再是从根区域扫描,G1 GC扫描s区直接可达的老年代对象,并将这些对象标记;
  • 其次是并发标记,在整个堆中进行并发标记,若在该阶段中发现垃圾,那这个区域会立即被回收;
  • 接下来是再次标记的过程,修正上次标记的结果,此工程需要stw;
  • 再接下来是独占清理,计算各个区域的存活对象和GC的回收比例,会触发stw;
  • 最后是并发清理阶段,识别并清理完全空闲的区域。

混合回收的过程如下:

  • 采用复制算法,将新生代的垃圾和老年代的垃圾混合回收,老年代只有一部分,而不是全部的老年代。混合回收的算法个新生代中的回收算法完全一样,只是回收集多了老年代的内存分段。

因此在G1回收器中,需要避免使用-Xmn或者-XX:NewRatio等相关选项显式设置新生代大小,因为固定新生代会覆盖暂停时间的目标,并且堆暂停时间也不要过于苛刻

7,经典垃圾回收器总结

通过上面详细的对各种垃圾回收器的描述,因此将各个回收器的特点整理如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKAy8hCx-1682412834189)(img/1682411248279.png)]

垃圾回收器如何选择

1,优先调整堆的大小让服务器自己来选择
2,如果内存小于100M,使用串行收集器
3,如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
4,如果允许停顿时间超过1秒,选择并行或者JVM自己选
5,如果响应时间最重要,并且不能超过1秒,使用并发收集器
6,4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

8,GC日志的常用参数

-XX:+PrintGC                      //输出GC日志类
-XX:+PrintGCDetails               //输出Gc的详细信息
-XX:+PrintGCTimeStamps            //输出GC的时间戳
-XX:+PrintGCDateStamps            //输出Gc的时间戳
-XX:+PrintHeapAtGC                //在GC前后打印出堆信息
-Xloggc:../logs/gc.log            //设置日志的输出路径

如若转载,请附上转载链接地址:https://blog.csdn.net/zhenghuishengq/article/details/130369011

猜你喜欢

转载自blog.csdn.net/zhenghuishengq/article/details/130369011