浅谈GC标记清除算法简述。串行GC,并行GC,CMS GC,G1 GC总结,这么多GC,该选择使用哪个GC?

本文深入探讨了Java的垃圾收集机制,包括为何需要GC、标记清除算法、并发标记清理(CMS)与G1GC、ZGC和ShenandoahGC等低延迟GC,以及如何根据系统需求选择合适的GC策略。讲解了不同GC的工作原理,如串行GC、并行GC和并发GC,并分析了CMS的MaxNewSize与MaxHeapSize的关系。此外,还介绍了AdaptiveSizePolicy如何动态调整堆内存区域的大小。

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

GC(Garbage Collection)。虽然很多人将GC称之为垃圾回收器,但是它完成的不仅是回收,更多的工作是在管理

标记清除算法

一说到Java,自然离不开GC(Garbage Collection)。

为什么需要GC

根据Java内存模型,其中程序计数器虚拟机栈本地方法栈3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。

而Java的堆和方法区(从Java8开始被Metaspace代替)有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。GC所关注的正是这部分内存该如何管理。

堆中存放的就是对象,为了减少内存的消耗,对于已经死亡(不在被使用)的对象,我们需要将其清理掉,腾出空间为其它对象使用,这便是GC需要做的事情,那如何判断对象是否死亡呢?

很直白的一种方式是引用计数法:如果一个对象被引用了一次计数值就加1,使用完毕,计数值减一。当一个对象的计数值为0时,就意味着它不再被使用,就可以将其清理掉。

但是这样的方式存在一个问题,那便是环状依赖

如下图所示,对象B和对象C之间相互引用,但是除此之外,它们没有引用其它对象,也没有其它对象引用它们,这种情况下,这两个对象本应该宣布死亡,然后被回收,但是由于这两个对象互相之间还有引用,这导致计数值不为0,这两个对象就不会被清理掉。
在这里插入图片描述

标记(Mark)清除(Sweep)算法

计数法不能解决环状依赖的问题,那有没有其它方法呢?当然有,答案就是标记清除算法。当前主流的GC几乎都是使用标记清除算法来判断对象是否存活,从根节点出发,不可达的对象便是被认为死亡的对象,就可以将其清理。

  • Marking(标记): 遍历所有的可达对象,并在本地内存(native)中分门别类记下。
  • Sweeping(清除): 这一步保证了,不可达对象所占用的内存,在之后进行内存分配时可以重用。

GC ROOTS开始标记可达对象,比如根对象引用了A对象,因GC ROOTS可以到达对象A,因此堆root和对象A进行标记。由于对象B和对象C之间是循环依赖,没有其它路径可以到达对象B或者对象A,因此没法进行标记。标记的对象是仍在使用的对象,是存活对象,因此不用清除,而没有标记的对象则进行清除。这样就能解决使用循环计数法,环状依赖导致的不能被清除的问题。

但是,由于对象之间的依赖关系在运行过程中是时刻变化的,如果有上百万个对象,要对其进程标记,确定可不可达很困难,可能这个时刻这个对象还处于使用状态,下一个时刻它就处于死亡状态了。怎么办?可以使用STW(Stop the World),也就是说让虚拟机暂停,让所有工作的线程都停止,停止运行以后,对象之间的变化就不会发生变化了,然后进行标记。
在这里插入图片描述
可以作为GC Roots的对象。

  1. 当前正在执行的方法里的局部变量和输入参数

在这里插入图片描述

  1. 活动线程(Active threads)

  2. 所有类的静态字段(static field)

  3. JNI 引用

除了清除,有的标记清除算法还需要做压缩,说白了就是将零星使用的内存变得连续紧凑。如下图所示。
在这里插入图片描述
GC为了对堆内存进行更加精细的管理,通常将堆分为两个区域:年轻代老年代。年轻代又可以进一步分为:新生代S0S1区。
在这里插入图片描述
老年代中的对象是存活时间较长的对象,新创建的对象在年轻代中,如果在年轻代中存活了一段时间仍未被GC清理,则将其移动到老年代中。年轻代又进一步划分为三个区,新生代S0S1。S0和S1是存活区,新生代,S0和S1区的最大内存之比默认为:8:1:1。

新生代如果存满了,就得执行一次GC,新生代中的数据大部分都是要被清除掉的,只有少部分的数据能存活下来,存活的数据被复制进S0或者S1。S0或者是S1总有一个是空的。假设S0为空,S1不为空,那么当新生代满了以后,新生代存活的数据以及S1中存活的数据就会被复制进S0,然后S1和新生代就被清空

  • 年轻代发生的的GC叫做Young GC(YGC),频率比较高,速度也比较快。

  • 老年代存活的时间比较长,GC频率比较低,只有等到为堆整体做一次GC(Full GC,简称FGC)的时候,才会为Old区做GC。

新的对象分配在年轻代的新生代区(Eden),在标记阶段,Eden区存活的数据就会被复制到存活区。

GC中对象的转移:

  • 新生代的对象转移到存活区使用的是复制

    年轻代分为了三个区,进行GC的时候,只需要把其余两个区域中存活对象的复制到一个存活区,然后清空其它区域即可

  • 老年代的对象转移采用的是复制

    老年代只有一个区,默认都是存活对象,不能复制以后进行暴力删除。整理老年代的空间中的内容(相当于是进行压缩),是将所有存活对象移动到老年代的开始位置进行存放,目的是不让内存使用碎片化。
    在这里插入图片描述

基于堆的划分,标记清除算法通常分为三种,分别适用于不同的堆内存区域。
在这里插入图片描述

串行GC和并行GC

在这里插入图片描述
Java8默认使用的是并行GC(ParallelGC)
在这里插入图片描述

CMS GC和G1 GC

CMS(Concurrent Mark Sweep):并发标记清理
G1(Garbage First)
并行GC和并发GC

  • 并行GC:说明同一时刻有多条线程在执行GC任务,通常默认此时用户的工作线程处于等待状态,也就是说执行GC任务的线程和用户的工作线程是不能共存的。对于并行GC而言,要么是不工作,要么是全部线程都用于GC。
  • 并发GC:执行GC任务的线程和用户的工作线程是能共存的。执行GC任务的线程占用了系统的部分资源,这就代表系统不能将所有的资源都用于程序工作,会导致系统的吞吐量收到一定的影响。
    在这里插入图片描述

在这里插入图片描述
执行流程
在这里插入图片描述
从Java9以后使用G1 GC作为默认的GC
在这里插入图片描述

ZGC和Shenandoah GC

Shenandoah GC 与 ZGC 同为新一代的低延迟收集器, 分别由RedHat和Oracle开发,两者非常相似。

在MAC 和Windows上,ZGC和Shenandoah GC在Java 15上才开始支持。在RedHat以及Centos上,Shenandoah GC可以反向支持到JDK 8。

这是一组Shenandoah GC的测试数据,从该数据中可以看出,Shenandoah GC的暂停时间远远低于之前的GC。
在这里插入图片描述
为什么能做到这么低的暂停时间呢?从下图可以看出Shenandoah GC的STW的时间远远低于前面的GC,Shenandoah GC多数的步骤都是在并发执行的。
在这里插入图片描述

Epsilon GC

从JDK 11中引进的,用于实验的GC。

如何选择GC

到目前为止,我们一共了解了如下几款GC

  1. 串行GC(Serial GC): 单线程执行,应用需要暂停;
  2. 并行GC(ParNew、Parallel Scavenge、Parallel Old): 多线程并行地执行垃圾回收,
    关注与高吞吐;
  3. CMS(Concurrent Mark-Sweep): 多线程并发标记和清除,关注与降低延迟;
  4. G1(G First): 通过划分多个内存区域做增量整理和回收,进一步降低延迟;
  5. ZGC(Z Garbage Collector): 通过着色指针和读屏障,实现几乎全部的并发执行,几毫秒级别的延迟,线性可扩展;
  6. Shenandoah: G1 的改进版本,跟ZGC 类似。
  7. Epsilon: 实验性的GC,供性能分析使用,实际上不会回收任何对象,仅在实验阶段使用

从中也可以看出GC的演进路线

  1. 串行 ➡​ 并行: 利用多核CPU 的优势,大幅降低GC 暂停时间,提升吞吐量。
  2. 并行 ➡ 并发: 不只开多个GC 线程并行回收,还将GC操作拆分为多个步骤,让很多繁重的任务和应用线程一起并发执行,减少了单次GC 暂停持续的时间,这能有效降低业务系统的延迟。
  3. CMS ➡ G1: G1 可以说是在CMS 基础上进行迭代和优化开发出来的,划分为多个小堆块进行增量回收,这样就更进一步地降低了单次GC 暂停的时间
  4. G1 ➡ ZGC。ZGC 号称无停顿垃圾收集器,这又是一次极大的改进。ZGC 和G1 有一些相似的地方,但是底层的算法
    和思想又有了全新的突破。

选择GC 的一般性的指导原则:

  1. 如果系统考虑吞吐优先(CPU的利用率),CPU 资源都用来最大程度处理业务,用Parallel GC;
  2. 如果系统考虑低延迟有限,每次GC 时间尽量短,用CMS GC;
  3. 如果系统内存堆较大,同时希望整体来看平均GC 时间可控,使用G1 GC。
  4. 一般4G 以上,内存算是比较大,用G1 的性价比较高。
  5. 一般超过8G,比如16G-64G 内存,非常推荐使用G1 GC。
  6. 如果内存非常大,128G或者是256G,则需要使用ZGC(Oracle)或者是Shenandoah GC(Red Hat)

常用的GC搭配
在这里插入图片描述

GC分析

AdaptiveSizePolicy

AdaptiveSizePolicy(自适应大小策略) 是 JVM GC Ergonomics(自适应调节策略) 的一部分。如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。

开启AdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy

关闭AdaptiveSizePolicy

-XX:-UseAdaptiveSizePolicy

在Java 8 HotSpot VM里,使用的是ParallelScavenge GC算法。ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)默认行为是SurvivorRatio如果不显式设置则使用AdaptiveSizePolicy来进行自动配置的。显式设置到默认的比例则会有效果

运行jar包时配置的启动参数:-Xmx用来设置最大堆内存,-Xms用来设置初始堆内存

java -Xmx1g -Xms1g -jar gateway-server-0.0.1-SNAPSHOT.jar --server.port=8088

Attaching to process ID 4443, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.271-b09

using thread-local object allocation.
Parallel GC with 2 thread(s)#Java8默认的是并行GC,默认的线程是CPU的核心数

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 1073741824 (1024.0MB) #VM配置的Xmx1g,Xms1g,如果没有配置Xmx这个参数,如果机器物理内存大于1G则默认为物理内存的1/4,如果小于1G,则默认为物理内存的1/2
   NewSize                  = 357564416 (341.0MB) #Young区此时所占的空间大小
   MaxNewSize               = 357564416 (341.0MB) #Young区最大空间
   OldSize                  = 716177408 (683.0MB) #OldSize = MaxHeapSize - NewSize
   NewRatio                 = 2 #Young区与Old区的最大容量的比值为1:2,也就是说MaxNewSize = MaxHeapSize / (1+2)
   SurvivorRatio            = 8 #Young区中Eden区与Survivor区的容量比例,
								#注意这是最大的容量比值,不代表时时刻刻Eden区占用的空间与Survivor区占用的空间大小都是8:1
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 268435456 (256.0MB) #既然SurvivorRatio是8,为什么这里的Eden和Survivor区的比例不是8:1呢?(256/42.5=6)
                                  #因为Java8使用的是ParallelGC(并行GC)默认开启了AdaptiveSizePolicy,会动态调整Eden和Survivor的大小
								  #可以显示配置-XX:SurvivorRatio=8使得EdenCapacity:SurvivorSpace = 8:1
								  #AdaptiveSizePolicy动态调整 Eden、Survivor 区的大小,存在将 Survivor 区调小的可能
								  #使用-XX:+UseConcMarkSweepGC(CMS GC)也会默认关闭AdaptiveSizePolicy
   used     = 145378800 (138.64402770996094MB)
   free     = 123056656 (117.35597229003906MB)
   54.15782332420349% used
From Space:
   capacity = 44564480 (42.5MB) # Survivor区有两个,不为空的那个作为From,空的那个作为To
   used     = 17048504 (16.25872039794922MB)
   free     = 27515976 (26.24127960205078MB)
   38.25581270105698% used
To Space:
   capacity = 44564480 (42.5MB)
   used     = 0 (0.0MB)
   free     = 44564480 (42.5MB)
   0.0% used
PS Old Generation
   capacity = 716177408 (683.0MB)
   used     = 5218952 (4.977180480957031MB)
   free     = 710958456 (678.022819519043MB)
   0.7287233500669152% used

15753 interned Strings occupying 2109568 bytes.

关于CMS GC的MaxNewSize

CMS GC的MaxNewSizeMaxHeapSize并不是1 : 3的关系。

最大Young区只与并发的线程数有关

计算公式:64M * 并发GC线程数 * 13 / 10

-XX:ParallelGCThreads=2

64M * 2* 13 / 10 = 166

root@simon-computer:/usr/samba-share/java# java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=2 -jar gateway-server-0.0.1-SNAPSHOT.jar
10152 gateway-server-0.0.1-SNAPSHOT.jar
······
root@simon-computer:/home/simon# jmap -heap 10152
······
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 174456832 (166.375MB)
   MaxNewSize               = 174456832 (166.375MB)
   OldSize                  = 899284992 (857.625MB)

-XX:ParallelGCThreads=3

64M * 3* 13 / 10 = 249

root@simon-computer:/usr/samba-share/java# java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=3 -jar gateway-server-0.0.1-SNAPSHOT.jar
......
root@simon-computer:/home/simon# jmap -heap 10417
......
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 261685248 (249.5625MB)
   MaxNewSize               = 261685248 (249.5625MB)
   OldSize                  = 812056576 (774.4375MB)

-XX:ParallelGCThreads=4

root@simon-computer:/usr/samba-share/java# java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -jar gateway-server-0.0.1-SNAPSHOT.jar
......
root@simon-computer:/home/simon# jmap -heap 10046
......
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 348913664 (332.75MB)
   MaxNewSize               = 348913664 (332.75MB)
   OldSize                  = 724828160 (691.25MB)

-XX:ParallelGCThreads=6

由于此机器CPU的核心总共只有4个,只能产生4个线程,因此当并发线程大于4时,会按照NewRatio的比例来进行设置

341 / 1024 = 1/3

root@simon-computer:/usr/samba-share/java# java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=6 -jar gateway-server-0.0.1-SNAPSHOT.jar
······
root@simon-computer:/home/simon# jmap -heap 10251
······
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 357892096 (341.3125MB)
   MaxNewSize               = 357892096 (341.3125MB)
   OldSize                  = 715849728 (682.6875MB)
   NewRatio                 = 2

参考极客时间Java训练营

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值