14_垃圾回收器 ParNew 、CMS
1921字约6分钟
2024-08-10
新生代垃圾回收器 ParNew
ParNew 介绍
新生代的 ParNew
垃圾回收器主打的是多线程回收机制,另外一种 Serial
垃圾回收器主打的是单线程垃圾回收,
他们都是回收新生代的,垃圾回收都是采用的复制算法,唯一区别就是一个是单线程和一个是多线程
指定垃圾回收器为 ParNew
在 java -jar
命令后面跟上 JVM 启动参数 -XX:+UseParNewGC
即可
ParNew
垃圾回收器默认设置的垃圾回收线程数量就是跟 CPU
的核数是一样的,一般不需要我们手动调节
老年代垃圾回收器 CMS
CMS 垃圾回收基本原理
CMS
垃圾回收器采用的是 标记-清理算法,标记哪些对象是存活对象,然后把垃圾对象清理掉
进行 Full GC
时,标记-清理算法中的标记,就是看各个对象是否被 GC Roots
引用了,是的话就是存活对象,否则就是垃圾对象
先将存活对象标记出来,然后一次性把垃圾对象都回收掉,如下图
这种方法有个很大的问题,就是会造成很多内存碎片,这些内存碎片可能放不下任何一个对象,这个内存就被浪费了
CMS 如何让系统工作的同时进行垃圾回收
之前我们说过 Stop the World
的问题,进行垃圾回收的时候,我们的系统工作线程是暂停工作了的,垃圾回收器肯定是想优化这个点的
CMS
垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模型来处理的
CMS
在执行一次垃圾回收的过程分为 4
个阶段:
1、初始标记
2、并发标记
3、重新标记
4、并发清理
初始标记
执行初始标记阶段,会让系统的工作线程全部停止,进入 Stop the World
状态,将所有 GC Roots
直接引用的对象标记
第一阶段虽然说会造成 Stop the World
暂停系统工作线程,但是影响不大,仅仅标记 GC Roots
直接引用的对象速度很快
并发标记
这个阶段系统的工作线程不会停止,这个期间可能会创建新的存活对象,也可能有部分存活对象失去引用成为垃圾对象
在这个阶段垃圾回收线程会尽可能的对已有的对象进行 GC Roots
追踪
对老年代所有对象进行 GC Roots
追踪是最耗时的,需要追踪所有对象是否从根源上被 GC Roots
引用了
但是这个最耗时的阶段是跟系统程序并发运行的,所以不会对系统运行造成影响
重新标记
第二阶段结束后,可能会有很多存活对象和垃圾对象是第二阶段没有标记的
所以在重新标记阶段,要让系统程序工作线程停下来,再次进入 Stop the World
状态
重新标记第二阶段里新创建的对象,以及已有对象失去引用变成垃圾对象
重新标记阶段速度很快,是对第二阶段中被系统程序运行变动过的少数对象进行标记
并发清理
第三阶段执行完后,重新恢复系统程序的运行,开始执行并发清理
这个阶段进行对象的清理,虽然很耗时,但是是和系统程序并发运行,不会对系统运行造成影响
并发回收垃圾导致 CPU 资源紧张
并发标记对老年代所有对象进行 GC Roots
追踪,并发清理需要把垃圾对象从内存中各个位置清理掉,这两个阶段都是十分耗时的
CMS
垃圾回收器虽然能在垃圾回收的同时让系统工作,但是与系统工作线程同时工作,会导致有限的 CPU
资源被垃圾回收线程占用一部分
CMS
的垃圾回收线程在这两个阶段是比较耗费 CPU
资源的,CMS
默认启动的线程数量是 (CPU核数 + 3)/ 4
Concurrent Mode Failure 问题
在并发清理阶段,系统程序一直在运行,随时会有一些对象进入老年代,一些变成垃圾对象
CMS
只是清理回收之前标记好的垃圾对象,像清理阶段产生的垃圾对象是浮动垃圾
为了保证 CMS
垃圾回收期间,还有一定空间让一些对象进入老年代,一般会预留一定空间
-XX:CMSInitiatingOccupancyFaction
参数可以设置老年代占用多少比例时触发 CMS
垃圾回收,JDK1.6
默认值是 92%
如果 CMS
垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,此时就会发生 Concurrent Mode Failure
,意思就是并发垃圾回收失败了,一边回收,一边把对象放入老年代,内存不够了
此时就会自动用 Serial Old
垃圾回收器代替 CMS
,直接强行把系统程序 Stop the World
,重新进行长时间的 GC Roots
追踪,然后一次性把垃圾对象回收掉
内存碎片问题
标记-清理算法,每次回收垃圾对象之后,会产生大量的内存碎片
CMS
的 -XX:+UseCMSCompactAtFullCollection
参数默认是打开的,意思是在 Full GC
之后要再次进行 Stop the World
,停止工作线程然后进行碎片整理,把存活对象挪到一起,避免内存碎片
-XX:CMSFullGCsBeforeCompaction
参数默认是 0
,意思是执行多少次 Full GC
之后执行一次内存碎片整理工作,默认值就是每次 Full GC
之后都会进行一次内存整理
思考环节
1、用单线程垃圾回收好还是多线程垃圾回收好,是 Serial 垃圾回收器好还是 ParNew 拉机器好?
启动系统的时候可以区分服务器、客户端模式,启动系统的时候加入 -server
就是服务器模式,加入 -client
就是客户端模式
区别在于用单线程还是多线程回收垃圾,如果系统部署在 4核8G
的 Linux
服务器上,就应该用服务器模式(多线程),如果系统运行在 Windows
上的客户端程序,就应该是客户端模式(单线程)
2、为什么老年代的 Full GC 要比新生代的 Young GC 慢很多倍?
新生代直接从 GC Roots
触发追踪那些对象是存活的就行了,并且新生代存活对象很少,不需要追踪多少对象,速度很快,然后直接把存活对象放入 Survivor
中,一次性回收 Eden 和之前使用的 Survivor
就完事了
CMS
的 Full GC
在并发标记阶段需要追踪所有存活对象,老年代对象很多,过程就很慢。其次并发清理阶段不是一次性回收一大片内存,而是零零散散各个位置的垃圾对象,速度很慢。最后完事了还要进行一次内存碎片整理,把大量存活对象挪到一起,就更慢了