【转帖】CMS垃圾回收器详解

CMS垃圾回收器详解

https://www.jianshu.com/p/08f0b85ad665

垃圾回收器组合

垃圾回收器从线程运行情况分类有三种:

  • 串行回收,Serial回收器,单线程回收,全程stw;
  • 并行回收,名称以Parallel开头的回收器,多线程回收,全程stw;
  • 并发回收,cms与G1,多线程分阶段回收,只有某阶段会stw;

CMS垃圾回收

CMS垃圾回收特点

  • cms只会回收老年代和永久带(1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),不会收集年轻带;
  • cms是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%;

CMS垃圾回收器工作原理

CMS的GC过程有6个阶段(4个并发,2个暂停其它应用程序):

1. 初次标记(STW initial mark):标记老年代中所有的GC Roots引用的对象;标记老年代中被年轻代中活着的对象引用的对象(初始标记也会扫描新生代);会导致stw。

2. 并发标记(Concurrent marking):从初次标记收集到的‘根’对象引用开始,遍历所有能被引用的对象。

3. 并发可中断预清理(Concurrent precleaning):改变当运行第二阶段时,由应用程序线程产生的对象引用,以更新第二阶段的结果。标记在并发标记阶段引用发生变化的对象,如果发现对象的引用发生变化,则JVM会标记堆的这个区域为Dirty Card。

那些能够从Dirty Card到达的对象也被标记(标记为存活),当标记做完后,这个Dirty Card区域就会消失。

4. 最终重新标记(STW remark):由于并发预处理是并发的,对象引用可能发生进一步变化。因此,应用程序线程会再一次被暂停(stw)以更新这些变化,并且在进行实际的清理之前确保一个正确的对象引用视图。这一阶段十分重要,因为必须避免收集到仍被引用的对象。

5. 并发清理(Concurrent sweeping):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。

6. 并发重置(Concurrent reset):CMS清除内部状态,为下次回收做准备。

问题思考

1. 并发预处理阶段意义何在?

并发预处理阶段做的工作还是标记,与4的重标记功能相似。既然相似为什么要有这一步?

前面我们讲过,CMS是以获取最短停顿时间为目的的GC。重标记需要STW(Stop The World),因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。

此阶段标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。

2. 如何确定老年代的对象是活着的?

答案很简单,通过GC ROOT TRACING可到达的对象就是活着的。

老年代进行GC时如何确保上图中Current Obj标记为活着的?答案是必须扫描新生代来确保。这也是为什么CMS虽然是老年代的gc,但仍要扫描新生代的原因。

在CMS日志中我们可以清楚地看到扫描日志:

[GC[YG occupancy: 820 K (6528 K)]

[Rescan (parallel) , 0.0024157 secs]

[weak refs processing, 0.0000143 secs]

[scrub string table, 0.0000258 secs] 

[1 CMS-remark: 479379K(515960K)] 480200K(522488K), 0.0025249 secs] 

[Times: user=0.01 sys=0.00, real=0.00 secs]

Rescan阶段(STW remark的一个子阶段)会扫描新生代和老年代中的对象。在日志中可以看到此阶段标识为Rescan (parallel),说明此阶段是并行进行的。

重点来了:全量的扫描新生代和老年代会不会很慢?肯定会。CMS号称是停顿时间最短的GC,如此长的停顿时间肯定是不能接受的。如何解决呢?那就是必须要有一个能够快速识别新生代和老年代活着的对象的机制。

新生代垃圾回收完剩下的对象全是活着的,并且活着的对象很少。如果能在并发可中断预清理阶段发生一次Minor GC,那STW remark的时间就会缩短很多。

CMS 有两个参数:CMSScheduleRemarkEdenSizeThreshold、CMSScheduleRemarkEdenPenetration,默认值分别是2M、50%。

  • -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,即当eden使用达到此值时,才会开始abortable-preclean阶段。
  • -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行。

所以两个参数组合起来的意思是eden空间使用超过2M时启动可中断的并发预清理,直到eden空间使用率达到50%时中断,进入remark阶段。

那可终止的预清理要执行多长时间来保证发生一次Minor GC呢?答案是没法保证。道理很简单,因为垃圾回收是JVM自动调度的,什么时候进行GC我们控制不了。

但此阶段总有一个执行时间吧。CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark。

如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。

这样做利弊都有:

  • 好的一面是减少了remark阶段的停顿时间;
  • 坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。

CMS日志如下:

7688.150: [CMS-concurrent-preclean-start]

7688.186: [CMS-concurrent-preclean: 0.034/0.035 secs]

7688.186: [CMS-concurrent-abortable-preclean-start]

7688.465: [GC 7688.465: [ParNew: 1040940K->1464K(1044544K), 0.0165840 secs] 1343593K->304365K(2093120K), 

0.0167509 secs]7690.093: [CMS-concurrent-abortable-preclean: 1.012/1.907 secs]  7690.095: [GC[YG occupancy: 522484 K (1044544 K)]

7690.095: [Rescan (parallel) , 0.3665541 secs]7690.462: [weak refs processing, 0.0003850 secs] [1 CMS-remark: 302901K(1048576K)] 825385K(2093120K), 0.3670690 secs]

7688.186启动了可终止的预清理,在随后的三秒内启动了Minor GC,然后进入了Remark阶段。

实际上为了减少remark阶段的STW时间,预清理阶段会尽可能多做一些事情来减少remark停顿时间。remark的rescan阶段是多线程的,为了便于多线程扫描新生代。

3. 进行Minor GC时如果有老年代引用新生代,怎么识别?

有研究表明,在所有的引用中,老年代引用新生代这种场景不足1%。

CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。

并发标记时,如果某个对象的引用发生了变化,就标记该对象所在的块为 dirty card。并发预清理阶段就会重新扫描该块,将该对象引用的对象标识为可达。

当有老年代引用新生代,对应的card table被标识为相应的值(card table中是一个byte,有八位,约定好每一位的含义就可区分哪个是引用新生代,哪个是并发标记阶段修改过的。所以,Minor GC通过扫描card table就可以很快的识别老年代引用新生代。

关于CMS的JVM参数调优

第一次调优

运营一段时间后,发现CMSGC超过一秒的情况非常多,GC日志:

可以看出,在remark中的Rescan阶段耗费了1.57秒,并且这个过程是会导致应用暂停的。问题定位在了Rescan阶段。

发现在Rescan时新生代过大(4313641 K(7188480 K)),是导致Rescan慢的关键原因,如果能尽量保持新生代很小的时候就终止preclean阶段,就可以控制住在Rescan时新生代的大小。

查看JVM参数发现-XX:CMSScheduleRemarkEdenPenetration的意思是当新生代存活对象占EdenSpace的比例超过多少时,终止preclean阶段并进入remark阶段。这个参数的默认值是50%,按照现在的配置,就是7800m*50%=3900m左右,所以更改此参数设置为:-XX:CMSScheduleRemarkEdenPenetration=1

进行压力测试,发现remark阶段的耗时确实降低了不少,说明优化有效。

第二次调优

运行几天后观察GC日志(2011-09-05),发现每隔100000秒的CMSGC的峰值情况确实大大降低了,但是还是偶尔有超过1~2秒的CMSGC情况:

GC日志:

发现concurrent-abortable-preclean阶段超过了-XX:CMSMaxAbortablePrecleanTime设置的最大值10秒,所以强制终止了preclean阶段而进入remark阶段。而这段时间的两次ParNew之间的间隔了17秒之多。希望的是在preclean阶段产生一次MinorGC,所以将preclean的最大时长调整为30秒:-XX:CMSMaxAbortablePrecleanTime=30000

第三次调优

运行一段时间后,发现居然出现了FullGC,大概在3~5天左右出现一次,以下是FullGC时的日志:

发现在443310秒有promotion failed出现(新生代晋升到老生代空间不足导致的FullGC),但是此时的OldGen可以算出还剩1.45G的空间(5324800K-3871691K=1453109K),而根据gcLogViewer的统计,每次MinorGC后平均新生代晋升到老生代的内存大小仅为58K。所以并不是OldGen空间不够,而是OldGen的连续空间不够造成的promotion failed。

换句话说,是由于OldGen在距离上次CMSGC后,又产生了大量内存碎片,当某个时间点在OldGen中的连续空间没有一块足够58K的话,就会导致的promotion failed。

考虑如果能够缩短CMSGC的周期,保证在出现promotion failed之前就进行CMSGC,就可以避免这个问题了。所以考虑将新生代空间缩小(相对来说就增加了老生代的空间),并且将CMSGC触发比率降低,同时保证Survivor空间不变。所以优化参数改动如下:

-Xmn7800m -> -Xmn7020m
-XX:SurvivorRatio=8 –> -XX:SurvivorRatio=7
-XX:CMSInitiatingOccupancyFraction=80 -> -XX:CMSInitiatingOccupancyFraction=70

第四次调优

上面的调优保持系统稳定运行了很长时间后,突然有一台机器出现大量FullGC,观察gc.log发现是由于持久带满造成的:

应对的方法为加大持久带,并让持久带也使用CMSGC方式回收:

-XX:PermSize=64m  ->  -XX:PermSize=200m
-XX:MaxPermSize=128m -> -XX:MaxPermSize=200m
-XX:+CMSClassUnloadingEnabled

引用自:https://m.aliyun.com/yunqi/articles/54413

原文地址:https://www.cnblogs.com/jinanxiaolaohu/p/12122183.html

时间: 2024-10-12 23:53:16

【转帖】CMS垃圾回收器详解的相关文章

深入理解JVM虚拟机3:垃圾回收器详解

JVM GC基本原理与GC算法 微信公众号[Java技术江湖]一位阿里 Java 工程师的技术小站.作者黄小斜,专注 Java 相关技术:SSM.SpringBoot.MySQL.分布式.中间件.集群.Linux.网络.多线程,偶尔讲点Docker.ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!(关注公众号后回复”Java“即可领取 Java基础.进阶.项目和架构师等免费学习资料,更有数据库.分布式.微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Jav

JVM 垃圾回收器详解

1, 串行回收器 1.1, 新生代串行回收器 (1)特点:  –它仅仅使用单线程进行垃圾回收  –它是独占式的垃圾回收  –进行垃圾回收时, Java应用程序中的线程都需要暂停(Stop-The-World)  –使用复制算法  –适合CPU等硬件不是很好的场合 (2)设置参数:  -XX:+UseSerialGC 指定新生使用新生代串行收集器和老年代串行收集器, 当以client模式运行时, 它是默认的垃圾收集器 1.2, 老年代串行回收器 (1)特点:  –同新生代串行回收器一样, 单线程,

JVM 的垃圾回收器详解

Parallel Scavenge(Paraller):Parallel Scavenge和ParNew关注的点不一样:ParNew关注的是尽可能缩短暂停的时间,Parallel Scavenge关注的是吞吐量吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)举个例子:虚拟机运行100分钟,GC用了1分钟,吞吐量则为99%.停顿时间短的适合用户交互的程序(web前端应用),提高用户体验度.关注吞吐量,什么样的程序下关注吞吐量而不是非常注重交互及时性,主要是这种后台计算任务.Para

Java虚拟机之垃圾回收详解一

Java虚拟机之垃圾回收详解一 Java技术和JVM(Java虚拟机) 一.Java技术概述: Java是一门编程语言,是一种计算平台,是SUN公司于1995年首次发布.它是Java程序的技术基础,这些程序包括:实用程序.游戏.商业应用程序.在全世界范围内,Java运行在超过数十亿台个人计算机上,数十亿台设备上,还包括手机和电视设备.Java由一系列的关键组件作为一个整体构建出了Java平台. Java Runtime Edition 当你下载Java,你就得到了Java运行环境(JRE).JR

深入理解JVM之垃圾回收详解

一.垃圾收集的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾.JVM的一个系统级线程会自动释放该内存块.垃圾收集意味着程序不再需要的对象是"无用信息",这些信息将被丢弃.当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用.事实上,除了释放没用的对象,垃圾收集也可以清除内存记录碎片.由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内

python 垃圾回收详解

原文:https://zhuanlan.zhihu.com/p/31150408 总纲 策略和垃圾回收系统工作内容 引用计数详解 标记-清除+分代收集 循环引用 编程应用-常见方法 ex 过程详解 使用建议 触发机制 参考文献 Python垃圾回收机制--完美讲解(Python vs Ruby) Python垃圾回收机制及gc模块详解 原文地址:https://www.cnblogs.com/guolei2570/p/8832046.html

C# using垃圾回收详解

简介 定义一个范围,将在此范围之外释放一个或多个对象. 语法 using (Font font1 = new Font("Arial", 10.0f)) { } C# 语言参考 主要用途 using 关键字有两个主要用途: 作为语句 用于定义一个范围,在此范围的末尾将释放对象 C# 通过 .NET Framework 公共语言运行库 (CLR) 自动释放用于存储不再需要的对象的内存.内存的释放具有不确定性:一旦 CLR 决定 执行垃圾回收,就会释放内存.但是,通常最好尽快释放诸如文件句

[转帖]linux screen 命令详解,xshell关掉窗口或者断开连接,查看断开前执行的命令

linux screen 命令详解,xshell关掉窗口或者断开连接,查看断开前执行的命令 https://binwaer.com/post/12.html yun install -y screen screen -S zhaobsh ping www.163.com 关闭xshell 再次连接 screen -ls 根据 pid 使用 screen -r pid 再次打开连接. 一.背景 系统管理员经常需要SSH 或者telent 远程登录到linux 服务器,经常运行一些需要很长时间才能完

Kooboo CMS 之TextContent详解

TextCotent 在Kooboo.CMS.Content下面,在View中有使用到这个模型层. TextContent继承了ContentBase,而ContentBase是由2个部分类组成的,一个是内容对象基类的子类,另一个是实现了持久化.那...我们该如何去理解呢,这里我用PS画一幅图大家就懂了.TextContent有3个构造函数,其中这2个构造函数都和基类(ContentBase)有关系.这3个构造函数分别如下: /// <summary> /// Initializes a ne