当前位置:网站首页>如何理解CMS回收器降低gc停顿时间
如何理解CMS回收器降低gc停顿时间
2022-07-28 05:30:00 【georgesnoopy】
不管采用什么回收算法,垃圾回收都会包含两个大的过程:标记、回收。
- 标记:标记的目的就是识别出哪些对象是垃圾了。这个阶段按道理一定是STW的,否则就标记不出所有的垃圾对象,因为如果有并发,一边标记、一边会有垃圾产生,这个理论上永远标记不完。
- 回收:就是将标记出的垃圾对象回收,腾出内存。这个阶段分回收算法。这个看算法,比如标记-清理算法,在清理阶段是不需要STW的,因为清理阶段只是标记内存可重用就好了。而标记-整理我理解是需要的,因为整理的时候会改变对象的内存地址,同理复制算法也是。
分代收集的理论基础:标记-清除、标记-整理、复制这些垃圾回收算法各自有各自的优缺点,找不到一个完美的收集算法。那么就在不同的场景使用不同的算法,来发挥收集算法的有点,避免其缺点。在程序运行过程中,根据对象的特点,大概可以分为两类:
- 朝不保夕的对象,即从产生到变成垃圾时间是比较短的,比如局部对象。这种对象经过一次gc后,有非常大的概率被回收掉,回收效果非常可观。
- 驻留内存时间比较长的,比如全局对象,如连接池对象等。ps:其实还有一种特殊情况,就是占用空间特别大的对象。
jvm的分代收集就是将整个堆划分成多个两个区域:分别用来存储朝不保夕的对象和长时间驻留内存的对象
- 年轻代:这个区域用来存储朝不保夕的对象,因为这种对象存活时间比较段,所以这个区域一次gc后,大量垃圾被回收,存活下来的对象比较少。所以这是比较适合用复制算法的。但是复制算法需要将内存一份为二,平时只能使用一半,另一半用来回收复制存活对象。所以jvm将新生代进一步分为三部分:eden、from survivor、to survivor。
- 新建对象的时候,总是在eden去分配内存。
- 发生gc时,就将eden区存活的对象都copy到to survivor区,整个eden区就清空了。ps:from survivor区的作用啥是?这个没太get到。平时from和to只用一个yong gc的时候将eden和from的copy到to,然后from相当于就空闲了,后面使用的就是eden和to,下次gc的时候就将eden和原来的to的内存copy到from,也就是说from 和to来回切换,这比直接是使用一半、空闲一半用于gc,内存效率要更高
- 老年代:存储驻留内存时间比较长的对象,或者内存占用量比较大的对象。常驻内存对象不适合用复制算法:因为每次gc复制的时候都会将它复制一次,浪费时间;大对象不适合复制算法,因为占用内存大,复制代价也就大。所以老年代更适合标记-清理、标记-整理算法。
- 如何判断一个对象是驻留时间长的对象:jvm给每个对象都分配了一个分代年龄,每经历一次gc后,这个对象还存活着的时候,该对象分代年龄+1,当达到阈值的时候,将对象搬到老年代(默认是15,有参数可配置)
- 如何判断一个对象是占用内存比较大的对象:参数控制,当对象占用内存超过这个参数阈值的时候,就认为是大对象,直接放到老年代。
cms垃圾回收器
cms:Concurrent Mark Sweep,降低停顿的思路:将标记和回收两个阶段进一步分细,只是在最必要的阶段进行STW。
ps:明确一下:SWT指的停顿啥?是指业务线程停顿下来,gc线程独享cpu来执行垃圾回收。
首先它是一个老年代收集器,所以他肯定不适合复制算法,所以它采用的是标记-清除算法。为了减少停顿,将收集过程分段,在必要的时候STW就好。
CMS垃圾回收的过程
CMS垃圾回收过程主要分为如下4个步骤:

其中初始标记和重新标记需要STW,其他都是和用户进程并发运行的。
ps:为什么这么分阶段可以降低STW??
标记阶段
如前述,要想让标记阶段绝对准确,能够标记处所有的垃圾对象,那标记阶段就只能是STW。CMS将标记阶段进一步拆分成三步,在标记的准确性和停顿时间做了一个均衡。
这个均衡就是:初始标记和重新标记阶段STW,并发标记阶段不STW,减少标记过程中的停顿,但代价就是并发标记阶段会产生垃圾(这部分称之为浮动垃圾),CMS是无法处理浮动垃圾的,只能下次gc的时候再处理。
1. 初始标记
这个过程是需要STW,但是这个阶段不会遍历整个堆,而是只是标记出出GC Root直接引用到的那些对象,所以是非常快的。
2. 并发标记
这个阶段就是从GC Roots遍历整个堆,找到存活对象。这个过程gc线程和用户线程是并行运行的。所以在这个阶段用户线程可能产生新的垃圾,这种垃圾成为浮动垃圾,CMS在一次gc过程中是处理不了本次GC产生的浮动垃圾的。
在并发标记完整个堆后,实际上还有一个并发预清理阶段,这个阶段也是和业务线程并发执行的,所以也可以认为是在并发标记阶段。
并发预清理阶段会标记新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
在回收老年代的时候,除了扫描老年代以外,还需要扫描年轻代,以确认是否有年轻代对象引用了老年代,从而确定老年代对象到底是不是垃圾对象。ps:所以CMS虽然是老年代收集器,它也会去扫描年轻代的。
在并发标记阶段,gc线程和业务线程并发执行,如果在这个阶段不去做些什么事情,到了remark阶段才去全量扫描年轻代,那么remark的STW就会比较长,所以为了减少remark阶段的STW时间,在这个阶段会尽量去进行一次yong gc,然后到了remark阶段去扫描年轻代的时候,由于存活的对象比较少,扫描就会很快
CMS提供了两个参数:CMSScheduleRemarkEdenSizeThreshold(默认值2M)和CMSScheduleRemarkEdenPenetration(默认值50%)来控制是否在老年代gc前发起一次yong gc,组合起来表示的是:eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段。
这个可终止的预清理指的就是,在这个阶段gc会等待发生一次yong gc,但是又不能一直等待,当等待超过一定时间的时候,就终止等待,进入重新标记阶段,这个时间阈值就是CMSMaxAbortablePrecleanTime参数控制(默认5s)
另外,CMS还提供了一个参数CMSScavengeBeforeRemark,设置为true时,在进入remark阶段回强制进行一次yong gc。
另外,为了让remark阶段扫描全量新生代可以多线程并发扫描,在这个预清理阶段还会将新生代划分成多个块,这样remark阶段去rescan的时候,一个线程负责扫描一些块,这样就会更快的扫描完成,这个分块的过程也是在cms-concurrent-abortable-preclean发生minor gc的时候做的,如果在进入remark前没有发生minor gc,那么就是整个年轻代就是一整块。
3.重新标记(remark)
在这个阶段回去扫描年轻代以及老年代,确认哪些对象还是存活的,由于有了前面两个阶段的准备工作,这个阶段也会很快完成。在gc 日志中,看到rescan这样的日志,它是重新标记的一个子阶段,就是去扫描堆的过程。
ps:STW的意思是阻塞业务线程,不是说gc是单线程运行,这个阶段gc线程并发执行的。
清理阶段
4.并发清理
清理标记成垃圾的对象。
CMS的局限性
正式由于CMS是一个使用标记-清楚算法的并发收集器,在降低停顿的同时,也会遇到的问题:
1. 老年代清理的时候,怎么判断是否有年轻代引用了老年代对象。 cms的方式就是取全量扫描新生代,为了减少新生代的扫描时间,在full gc之前,会尽量进行一次minor gc去清理年轻代(这个有参数控制)。理论:这个理论基础也是分代收集的理论基础,年轻代的对象都是朝不保夕的,一次yong gc会回收调非常多的对象,存活下来的很少,所以yong gc都采用了复制算法,而这里是认为yong gc过后存活对象少,年轻代的扫描就比较快了。
2. cms采用了标记-清理算法,这种算法本身就会产生很多内存碎片。
cms提供了参数,在经过几次full gc后,进行一次内存整理,将碎片内存进行整理。碎片整理是需要STW的,且耗时比较长
3. gc过程和业务线程并发执行,正常的业务线程运行是需要内存的,在gc的过程中就需要留一部分内存给业务线程使用,不能等到内存没了的时候,才full gc。cms提供参数当内存使用到达参数指定值的时候就进行full gc。CMS提供了参数CMSInitiatingOccupancyFraction(默认值92%),当堆使用率达到了这个值的时候,发生一次full gc,留下8%的内存给并发的时候业务线程使用;另外CMS还共了一个自适应的方式,会根据历史情况,预测来年代还需要多久会被填满,在填满之前及时进行一次垃圾回收,CMS提供了参数UseCMSInitiatingOccupancyOnly来控制是否使用这个字使用
4. 并发阶段产生的浮动垃圾,本次gc是处理不了的,cms实际也是没有处理浮动垃圾。
5. 并发阶段,gc线程是要耗资源的,势必会和业务线程争抢cpu。如何均衡:cg线程数=(cpu个数+1)/4,即gc约占整体资源的25%。
6. 对于大内存的jvm回收时间长、不可控
CMS垃圾收集器无法满足软实时(Soft Real-time)特性:即让 一次GC 停顿时间能大致控制在某个阈值以内,但是又不必像实时系统那样非常严格。这也是很多业务系统都有的诉求。
在过去的 JVM 设计中,堆内存被分割成几个区域 —— Eden、Survivor、Old 的大小都是预先划分好的,gc按照分代使用不同的回收算法执行垃圾回收,一次回收会回收当前分代的所有内存,比如yong gc,会回收年轻代内存;对于major gc,会回收所有的老年代内存。这对于大内存来说,一次gc可能停顿好几秒,这对业务系统来说不可接受。随着内存越大,这种问题也就越突出,比如一个64G的jvm,老年代可能会分配32G,那么一次回收32G的内存,停顿时间基本都要到秒级。
为了解决这个问题,G1就诞生了。CMS进行GC的单位是分代,即一次要回收整个分代的内存,所有导致时间长不可控。G1的思路就是将内存划分更多更小的可以独立执行垃圾回收的Region,去控制一次GC回收多少个Region来达到软实时的目的。
ps:在cms中为了在新生代回收的时候不扫描老年代,索引引入card table记录哪些老年代对象引用了新生代的对象,从而yong gc的时候可以单独回收新生代,不必扫描老年代就可以完成新生代内存的回收。那么G1中要实现每个Region能够单独回收,在每次回收的时候就需要知道是否有其他Region中的对象引用当前Region中的对象,为了全量扫描所有的Region,G1中就搞出了Rset来记录,避免全量扫描,从而实现每个Region能够独立回收。
ps:如何判断跨代被引用对象的存活的
分代收集可以根据不用区域存储对象的特点,采用不同的收集算法,最大程度利用上算法的缺点,规避缺点。
但是不管使用哪种算法,其实第一步都是标记,识别出垃圾对象,如果不管是收集那个区域,都是从gc root开始扫描标记,这个肯定是可以标记到所有对象的(像CMS为了减少停顿不处理浮动垃圾是不同收集器的一个特例),但如果是这样,不管哪个区域的gc都全量扫描整个堆,在标记阶段又没有利用上分代的优势。特别是在yong gc的时候,一般来说年轻代要比老年代小很多,那么在yong gc的时候去全量扫描老年代,有些代价过大
yong gc的时候,如何判断是否有老年代对象引用了新生代对象?
为了避免yong gc的时候全量扫描整个老年代,从而拉长整个GC的STW,jvm引入了card-table,采用空间换时间的方式。
card-table将老年代内存分为一个一个的2^n大小的(默认512B)的区域,成为卡页,在年轻代维护了一个card-table,其实card-table就是一个key-value结构,key=卡页的首地址、value=key指定的卡页中的对象是否引用了新生代对象。这样只需要扫描一遍card-table就可以知道是否有老年代对象引用了新生代的对象,就不需要全量扫描全量的老年代了。
由于年轻代的对象本省就是朝不保夕的,15次gc后依然存活的对象才会进入老年代,老年代对象引用年轻代其实是非常少的。
ps:hotspot 虚拟机使用字节码解释器、JIT编译器、 write barrier维护 card table,当字节码解释器或者JIT编译器更新了引用,就会触发write barrier操作card table.
回收老年代的时候,如何判断是否有年轻代对象引用了老年代?
这个一般就是扫描全量年轻代了,不过CMS为了提高扫描年轻代的效率,在发起老年代gc之前,会主动发起一次yong gc。理论基础:这个理论基础也是分代收集的理论基础,年轻代的对象都是朝不保夕的,一次yong gc会回收调非常多的对象,存活下来的很少,所以yong gc都采用了复制算法,而这里是认为yong gc过后存活对象少,年轻代的扫描就比较快了。
参考:https://www.cnblogs.com/Leo_wl/p/5393300.html#_labelTop
边栏推荐
猜你喜欢
随机推荐
Addition, deletion, check and modification of sequence table
VNC Timed out waiting for a response from the computer
Redis主从复制原理及配置
uniapp项目怎么连接手机真机调试
Starting point Chinese website font anti crawling technology web page can display numbers and letters, and the web page code is garbled or blank
远程访问云服务器上Neo4j等服务的本地网址
TOPK problem
Record the first article of so and so Xiao Lu
MOOC Weng Kai C language week 7: array operation: 1. array operation 2. Search 3. preliminary sorting
Shell -- first day homework
win下安装nessus
多进程(多核运算)Multiprocessing
MHA高可用配置及故障切换
Review of C language (variable parameters)
Qucs preliminary use guide (not Multism)
Open virtual machine kali2022.2 and install GVM
MySQL queries all descendant nodes under the parent node. When querying the user list, it is processed by multi-level (company) departments. According to reflection, it recurses the tree structure too
Basic knowledge of functions and special points
Small turtle C (Chapter 5 loop control structure program 567) break and continue statements
分解路径为目录名和文件名的方法









