CMS gc实践总结(纠正并发线程数)

本文深入探讨了CMS垃圾回收器的工作原理及配置方法,适用于追求低停顿时间的应用场景。文章详细介绍了CMS回收周期各阶段的特点,提供了关键参数的设置指导,并分析了常见问题及其解决方案。

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

首先感谢阿宝同学的帮助,我才对这个gc算法的调整有了一定的认识,而不是停留在过去仅仅了解的阶段。在读过sun的文档和跟阿宝讨论之后,做个小小的总结。
    CMS,全称Concurrent Low Pause Collector,是jdk1.4后期版本开始引入的新gc算法,在jdk5和jdk6中得到了进一步改进,它的主要适合场景是对响应时间的重要性需求大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且应用中存在比较多的长生命周期的对象的应用。CMS是用于对tenured generation的回收,也就是年老代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代。在我们的应用中,因为有缓存的存在,并且对于响应时间也有比较高的要求,因此希望能尝试使用CMS来替代默认的server型JVM使用的并行收集器,以便获得更短的垃圾回收的暂停时间,提高程序的响应性。
    CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停,它的收集周期是这样:
    初始标记(CMS-initial-mark) -> 并发标记(CMS-concurrent-mark) -> 重新标记(CMS-remark) -> 并发清除(CMS-concurrent-sweep) ->并发重设状态等待下次CMS的触发(CMS-concurrent-reset)。
    其中的1,3两个步骤需要暂停所有的应用程序线程的。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记之后,暂停所有应用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。

    而并发标记、并发清除、并发重设阶段的所谓并发,是指 一个或者多个垃圾回收线程和应用程序线程并发地运行 ,垃圾回收线程不会暂停应用程序的执行,如果你有多于一个处理器,那么并发收集线程将与应用线程在不同的处理器上运行,显然,这样的开销就是会降低应用的吞吐量。Remark阶段的 并行 ,是指暂停了所有应用程序后,启动一定数目的垃圾回收进程进行并行标记,此时的应用线程是暂停的。

    CMS的young generation的回收采用的仍然是并行复制收集器,这个跟Paralle gc算法是一致的。

    下面是参数介绍和遇到的问题总结,

1、启用CMS: -XX:+UseConcMarkSweepGC 。 咳咳,这里犯过一个低级错误,竟然将+号写成了-号

2。CMS默认启动的回收线程数目是  (ParallelGCThreads + 3)/4)  ,如果你需要明确设定,可以通过-XX: ParallelCMSThreads =20来设定,其中 ParallelGCThreads是年轻代的并行收集线程数

3、CMS是不会整理堆碎片的,因此为了防止堆碎片引起full gc,通过会开启CMS阶段进行合并碎片选项: -XX:+UseCMSCompactAtFullCollection ,开启这个选项一定程度上会影响性能,阿宝的blog里说也许可以通过配置适当的CMSFullGCsBeforeCompaction来调整性能,未实践。

4.为了减少第二次暂停的时间,开启并行remark:  -XX:+CMSParallelRemarkEnabled ,如果remark还是过长的话,可以开启 -XX:+CMSScavengeBeforeRemark 选项,强制remark之前开始一次minor gc,减少remark的暂停时间,但是在remark之后也将立即开始又一次minor gc。

5.为了避免Perm区满引起的full gc,建议开启CMS回收Perm区选项:

+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled


6.默认CMS是在tenured generation沾满68%的时候开始进行CMS收集,如果你的年老代增长不是那么快,并且希望降低CMS次数的话,可以适当调高此值:
-XX:CMSInitiatingOccupancyFraction=80

这里修改成80%沾满的时候才开始CMS回收。

7.年轻代的并行收集线程数默认是 (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) ,如果你希望设定这个线程数,可以通过 -XX:ParallelGCThreads=  N 来调整。

8.进入重点,在初步设置了一些参数后,例如:
-server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=64m
-XX:MaxPermSize=64m -XX:-UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=80 -XX:+CMSParallelRemarkEnabled
-XX:SoftRefLRUPolicyMSPerMB=0

需要在生产环境或者压测环境中测量这些参数下系统的表现,这时候需要打开GC日志查看具体的信息,因此加上参数:

-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/home/test/logs/gc.log

在运行相当长一段时间内查看CMS的表现情况,CMS的日志输出类似这样:
4391.322: [GC [1 CMS-initial-mark: 655374K(1310720K)] 662197K(1546688K), 0.0303050 secs] [Times: user=0.02 sys=0.02, real=0.03 secs]
4391.352: [CMS-concurrent-mark-start]
4391.779: [CMS-concurrent-mark: 0.427/0.427 secs] [Times: user=1.24 sys=0.31, real=0.42 secs]
4391.779: [CMS-concurrent-preclean-start]
4391.821: [CMS-concurrent-preclean: 0.040/0.042 secs] [Times: user=0.13 sys=0.03, real=0.05 secs]
4391.821: [CMS-concurrent-abortable-preclean-start]
4392.511: [CMS-concurrent-abortable-preclean: 0.349/0.690 secs] [Times: user=2.02 sys=0.51, real=0.69 secs]
4392.516: [GC[YG occupancy: 111001 K (235968 K)]4392.516: [Rescan (parallel) , 0.0309960 secs]4392.547: [weak refs processing, 0.0417710 secs] [1 CMS-remark: 655734K(1310720K)] 766736K(1546688K), 0.0932010 secs] [Times: user=0.17 sys=0.00, real=0.09 secs]
4392.609: [CMS-concurrent-sweep-start]
4394.310: [CMS-concurrent-sweep: 1.595/1.701 secs] [Times: user=4.78 sys=1.05, real=1.70 secs]
4394.310: [CMS-concurrent-reset-start]
4394.364: [CMS-concurrent-reset: 0.054/0.054 secs] [Times: user=0.14 sys=0.06, real=0.06 secs]

其中可以看到CMS-initial-mark阶段暂停了0.0303050秒,而CMS-remark阶段暂停了0.0932010秒,因此两次暂停的总共时间是0.123506秒,也就是123毫秒左右。两次短暂停的时间之和在200以下可以称为正常现象。

但是你很可能遇到 两种fail引起full gc :Prommotion failed和Concurrent mode failed。

Prommotion failed的日志输出大概是这样:
 [ParNew (promotion failed): 320138K->320138K(353920K), 0.2365970 secs]42576.951: [CMS: 1139969K->1120688K(
2166784K), 9.2214860 secs] 1458785K->1120688K(2520704K), 9.4584090 secs]

这个问题的产生是由于救助空间不够,从而向年老代转移对象,年老代没有足够的空间来容纳这些对象,导致一次full gc的产生。解决这个问题的办法有两种完全相反的倾向: 增大救助空间、增大年老代或者去掉救助空间 。增大救助空间就是调整-XX:SurvivorRatio参数,这个参数是Eden区和Survivor区的大小比值,默认是32,也就是说Eden区是Survivor区的32倍大小,要注意Survivo是有两个区的,因此Surivivor其实占整个young genertation的1/34。调小这个参数将增大survivor区,让对象尽量在survitor区呆长一点,减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如65536)来做到。在我们的应用中,将young generation设置成256M,这个值相对来说比较大了,而救助空间设置成默认大小(1/34),从压测情况来看,没有出现prommotion failed的现象,年轻代比较大,从GC日志来看,minor gc的时间也在5-20毫秒内,还可以接受,因此暂不调整。

Concurrent mode failed的产生是由于CMS回收年老代的速度太慢,导致年老代在CMS完成前就被沾满,引起full gc,避免这个现象的产生就是调小 -XX:CMSInitiatingOccupancyFraction 参数的值,让CMS更早更频繁的触发,降低年老代被沾满的可能。我们的应用暂时负载比较低,在生产环境上年老代的增长非常缓慢,因此暂时设置此参数为80。在压测环境下,这个参数的表现还可以,没有出现过Concurrent mode failed。


参考资料:
JDK5.0垃圾收集优化之--Don't Pause 》 by 江南白衣
《记一次Java GC调整经历》 1 , 2  by Arbow
Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
Tuning Garbage Collection with the 5.0 JavaTM Virtual Machine
<think>我们正在讨论GC线程的概念及其在垃圾回收中的作用。根据提供的引用,我们可以整理出以下信息:1.引用[2]提到:线程是CPU调度的最小单位,进程是线程的集合。一个Java程序启动时,至少会有两个线程:主线程(执行main方法)和GC线程。2.引用[1]说明:垃圾回收器使用一组称为GC线程的线程来执行回收工作。JVM可能会动态调整GC线程的数量(过多或过少),这会影响性能。3.引用[4]描述了GC线程的工作方法:从GC根节点开始,递归遍历所有引用的对象,标记存活对象(弱引用不考虑在内),然后进行清理。因此,GC线程是JVM中专门负责垃圾回收的线程。它们由垃圾回收器管理,在后台自动运行,用于回收不再使用的对象占用的内存。接下来,我将详细解释GC线程的概念、作用和工作原理。###1.GC线程的概念在Java虚拟机(JVM)中,垃圾回收(GarbageCollection,GC)是一个自动管理内存的过程。GC线程是JVM创建并管理的特殊线程,专门负责执行垃圾回收任务。它们与应用程序线程(如主线程)并发并行运行,以确保堆内存得到有效回收[^2]。###2.GC线程的作用-**自动内存管理**:GC线程负责识别并回收不再被引用的对象(即“垃圾”),释放堆内存空间。-**避免内存泄漏**:通过定期回收无用对象,防止内存耗尽。-**减少程序员负担**:开发者无需手动释放内存,降低了内存管理错误的风险。###3.GC线程如何工作GC线程的工作过程通常分为以下阶段(以常见的标记-清除算法为例):1.**标记(Marking)**:-GC线程从一组根对象(GCRoot)开始遍历对象引用图。-根对象包括:静态变量、活动线程的栈帧中的局部变量、JNI引用等[^4]。-遍历过程中,所有从根对象可达的对象被标记为“存活”(live),其余对象视为垃圾。-此阶段会暂停所有应用程序线程(Stop-The-World,STW),以确保对象图的一致性。2.**清除(Sweeping)**:-GC线程遍历堆内存,回收未被标记的对象占用的空间。-回收的内存可重新分配给新对象。3.**压缩(Compacting,可选)**:-某些GC算法(如G1GC)会在清除后压缩内存,将存活对象移动到连续的内存区域,减少内存碎片。####并发并行GC现代JVM通常支持并发并行GC:-**并行GC**:多个GC线程同时工作,但应用程序线程在此期间完全暂停(STW)。-**并发GC**:GC线程与应用程序线程并发执行(如CMS、G1的部分阶段),减少STW时间。###4.GC线程数量的调整JVM会根据堆大小、CPU核心数等动态调整GC线程数[^1]:-**过多GC线程**:可能抢占应用程序线程的CPU资源,降低程序性能。-**过少GC线程**:可能导致GC时间过长,增加STW暂停时间。可通过JVM参数(如`-XX:ParallelGCThreads`)手动调整并行GC线程数。###总结GC线程是JVM垃圾回收的核心执行者,它们自动管理堆内存,通过标记-清除等算法回收垃圾对象。其工作可能涉及STW暂停,但现代GC算法(如G1、ZGC)通过并发处理尽量减少影响。合理配置GC线程数对优化应用性能至关重要[^1][^4]。```mermaidgraphTDA[GC线程启动]-->B[暂停应用线程STW]B-->C[标记阶段:遍历GCRoot及其引用]C-->D[恢复应用线程并发标记]D-->E[最终标记STW]E-->F[清除/回收阶段]F-->G[内存压缩可选]```§§相关问题§§1.如何监控和调整JVM的GC线程数量?2.不同垃圾回收器(如G1、CMS)的线程模型有何区别?3.如何减少GC过程中的Stop-The-World时间?</think>### GC线程的概念与作用 GC线程(Garbage Collection Threads)是JVM中专门负责垃圾回收的特殊工作线程。它们在内存管理系统中扮演核心角色,主要作用是通过自动回收不再使用的对象来释放堆内存空间,防止内存泄漏和溢出[^2][^4]。 #### 核心特征: 1. **独立于应用线程** GC线程由JVM在后台自动创建和管理,与执行Java代码的应用程序线程(如main线程)并行运行[^2]。 2. **并发/并行工作模式** 现代JVM通常使用多线程GC机制: - **并行回收**:多个GC线程同时工作(如Parallel GC) - **并发回收**:GC线程与应用线程交替执行(如CMS、G1 GC) 3. **动态线程分配** JVM会根据堆大小、CPU核心数等自动调整GC线程数量,可能过多或过少,影响性能[^1]。 --- ### GC线程的工作流程 以典型的**标记-清除算法**为例(引用[4]描述): #### 阶段1:标记存活对象 1. **根节点扫描** GC线程从GC Roots(静态变量、活动线程栈帧、JNI引用等)出发,建立待遍历队列[^4]。 ```mermaid graph LR A[GC Roots] --> B[线程栈帧] A --> C[静态变量] A --> D[JNI引用] ``` 2. **递归标记** - 遍历所有根节点引用的对象 - 递归标记子对象和子子对象 - 被标记对象视为存活(live) - **注意**:弱引用对象不会被标记[^4] #### 阶段2:回收内存 1. **计划阶段** 分析内存碎片情况,确定最优回收策略。 2. **清理阶段** - 删除所有未标记对象 - 整理内存空间(压缩阶段可选) - 更新内存分配表 --- ### GC线程的关键影响 1. **STW暂停(Stop-The-World)** 某些GC阶段(如初始标记)会暂停所有应用线程,GC线程数直接影响暂停时间[^1]。 2. **CPU资源竞争** 过多GC线程会抢占应用线程CPU资源,过少则延长回收时间[^1]。 3. **吞吐量权衡** 并行GC线程提高回收速度但增加开销,并发GC减少暂停但占用更多CPU周期。 --- ### 监控与调优 1. **查看GC线程数** 使用JVM参数:`-XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime` 2. **调整线程数** - 手动设置并行GC线程:`-XX:ParallelGCThreads=N` - 限制并发GC线程:`-XX:ConcGCThreads=N` 3. **优化建议** CPU密集型应用可减少GC线程,大堆应用需增加线程[^1]。 > **示例**:在8核服务器上,G1 GC默认线程数约为 $\frac{8}{4} = 2$(并发线程) + 5(并行线程) = 7个GC线程[^1]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值