本文详解了7种垃圾收集器,文章很干,适合用来面试前复习。建议收藏再看!
点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达
上一篇文章讲了垃圾回收算法,它是内存回收的方法论,垃圾收集器则是内存回收的具体实现。
我们面试中问到的应该都是 HotSpot 垃圾收集器。如下图一共有七种垃圾收集器,Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。
因为目前 JDK 1.8 中都是按照分代算法将内存分为了两块,我们也将垃圾收集器按照所在空间分类。上半部分区域为新生代收集器,下半部分是老年代收集器。两个收集器之间的连线代表它们可以搭配使用。
相关概念介绍
并行(Parallel)
并行(Parallel)指同时使用多个处理器或多个核心处理器来执行多个线程或进程的能力(一个CPU多个核也可以并行)。当系统有一个以上CPU时,多个线程在多个 CPU 上分别运行多个进程互不抢占 CPU 资源。
在垃圾回收过程中,并行指的是多条垃圾收集器线程并行工作,但此时用户线程仍然处于等待。
并发(Concurrent)
并发(Concurrent)指在同一时间间隔内处理多个独立任务的能力。
具体来说,当有多个任务需要处理时,系统会交替地执行它们,每个任务都会分配到一定的处理时间,看起来就好像它们是同时在运行一样。在并发处理中,多个任务的执行可以是交错的、并行的或串行的。
在垃圾回收过程中,并发指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集器程序运行于另一个CPU之上。
二者之间的关系:虽然并发和并行在某些情况下可以交替使用,但两者的概念是不同的。并发指多个任务在同一时间段内交替执行,而并行指使用多个处理器或核心同时执行多个任务。因此,并发的概念包含了并行,但并行并不一定是并发。
垃圾收集器分类
1.Serial收集器(新生代)
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,它只会使用一个CPU或者一条收集线程去完成垃圾收集操作,并且必须暂停其他所有的工作线程(”Stop the world“),直到它收集完成。Serial收集器是 Jvm client 模式下默认的新生代收集器。
优点:简单而高效,简单高效,适用于单核 CPU 环境和小型内存应用。Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的手机效率。
缺点:无法并行处理垃圾回收,会暂停应用线程进行垃圾回收,不适用于大型应用和高并发场景。并且如果是多核多CPU机器情况下,会造成机器处理性能的浪费。
2.Serial Old收集器(老年代)
Serial Old 收集器是 Serial 收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。它可以与所有类型的年轻代垃圾收集器搭配使用。
如果在Server模式下,那么它主要还有两大用途:
一种用途是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge收集器搭配使用。
另一种用途就是作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用,解决老年代的内存碎片问题。
它的优缺点可以参考 Serial 收集器。
3.ParNew收集器(新生代)
ParNew收集器其实就是 serial 收集器的多线程版本,使用复制算法,是年轻代的垃圾收集器。除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样,它的设计目标是尽可能减少应用程序的停顿时间。
ParNew收集器是运行在 Service 模式下虚拟机中首选的新生代收集器,其中一个与性能无关的原因就是除了 Serial 收集器外,目前多线程环境下,只有 ParNew 收集器能与 CMS 收集器配合工作(Serial 收集器效率太低不考虑)。
PreNew 收集器在单 CPU 环境中绝对没有 Serial 的效果好,由于存在线程交互的开销,该收集器在超线程技术实现的双 CPU 中都不能一定超过Serial收集器。默认开启的垃圾收集器线程数就是CPU数量,可通过 -XX:parallelGCThreads
参数来限制收集器线程数。
优缺点
优点
多线程收集:ParNew收集器使用多线程收集,可以利用多核CPU的优势,提高回收效率。
低停顿时间:ParNew收集器支持与CMS收集器配合使用,可以实现较低的停顿时间。
配合CMS收集器使用时,可以减少CMS初始标记和最终标记的时间。
缺点
只适用于新生代:ParNew收集器只能用于新生代,不能用于老年代。
依旧在垃圾回收时会产生较长的时间停顿。
总体来说,ParNew收集器主要适用于多核CPU的服务器应用,如果搭配CMS收集器使用,可以获得较低的停顿时间。但由于它只能用于新生代,因此在处理大量老年代对象时可能会有性能瓶颈。
4.Parallel Scavenge收集器(新生代)
Parallel Scavenge 收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。
吞吐量 = 程序运行时间/(程序运行时间 + 垃圾收集时间)
,虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。短停顿时间适合和用户交互的程序,体验好。高吞吐量适合高效利用CPU,主要用于后台运算不需要太多交互。
JVM 提供了两个参数来精确控制吞吐量:
最大垃圾收集器停顿时间(-XX:MaxGCPauseMillis 大于0的毫秒数,停顿时间小了就要牺牲相应的吞吐量和新生代空间).
设置吞吐量大小(-XX:GCTimeRatio 大于0小于100的整数,默认99,也就是允许最大1%的垃圾回收时间)。
还有一个参数表示自适应调节策略(GC Ergonomics)(-XX:UseAdaptiveSizePolicy)。就不用手动设置新生代大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)今生老年代对象大小(-XX:PretenureSizeThreshold),会根据当前系统的运行情况收集监控信息,动态调整停顿时间和吞吐量大小。
这也是其与 PreNew 收集器的一个重要区别,也是其无法与 CMS 收集器搭配使用的原因。它们的设计目标和运作方式不同。Parallel Scavenge 垃圾收集器的主要设计目标是提高吞吐量,通过多线程并行处理来实现高效的垃圾收集。CMS收集器尽可能地缩短垃圾收集时用户线程的停顿时间,以提升交互体验。它尽可能地让应用程序能够在垃圾收集期间继续运行。并且与 Hotspot VM的历史有关,Parallel Scanvenge是不在“分代框架”下开发的,而ParNew、CMS都是在分代框架下开发的。
5.Parallel Old收集器(老年代)
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法,JDK1.6才提供。其通常与 Parallel Scavenge 收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。
在执行 Old 区垃圾回收时,Parallel Old 会使用多个线程来同时扫描老年代中的对象,并进行垃圾回收。这些线程会同时处理不同区域的对象,从而使得回收操作能够并行执行,提高了回收效率。
6.CMS收集器(老年代)
CMS(Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是 HotSpot 虚拟机中的一款真正意义上的并发收集器,第一次实现了让垃圾回收线程和用户线程(基本上)同时工作。用CMS收集老年代的时候,新生代只能选择 Serial
或者 ParNew
收集器。
CMS收集器是基于标记-清除算法实现的,整个收集过程大致分为4个步骤:
初始标记(Initial Mark):在此阶段,CMS垃圾收集器会暂停应用程序线程(STW),标记出所有根对象(GC ROOTS)直接引用的对象,将这些对象标记为已使用。这个过程是并发的。
并发标记(Concurrent Mark):在此阶段,CMS垃圾收集器会与应用程序线程一起工作,标记出所有被根对象间接引用的对象,将这些对象标记为已使用。这个过程是并发的,与应用程序线程并发运行。
重新标记(Remark):在此阶段,CMS垃圾收集器会暂停应用程序线程,重新扫描所有被并发标记过的对象,并标记所有被遗漏的对象为已使用。这个过程是短暂的(会有STW)。
并发清除(Concurrent Sweep):在此阶段,CMS垃圾收集器会与应用程序线程一起工作,清除所有未被标记的对象。这个过程是并发的,与应用程序线程并发运行。
在进行完上述四个主要步骤之后,会有一个并发重置(concurrent-reset)的过程,CMS内部重置回收器状态,准备进入下一个并发回收周期。
其中初始标记、重新标记这两个步骤仍然需要停顿其他用户线程(Stop The World)。初始标记仅仅只是标记出 GC ROOTS 能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS 根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会被初始标记阶段稍长,但比并发标记阶段要短。
时间长短排序:并发标记 > 重新标记 > 初始标记
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS 优缺点
优点:
低停顿时间:CMS使用并发标记-清除算法,在标记和清除阶段都可以与应用程序同时运行,因此可以显著减少垃圾回收过程对应用程序的影响,实现低停顿时间的垃圾回收。
并发收集:CMS在垃圾回收过程中可以与应用程序同时运行,因此可以在不影响应用程序性能的情况下完成垃圾回收,提高应用程序的吞吐量。
可预测性:CMS通过在可控的时间段内执行垃圾回收来控制垃圾回收的影响,因此可以更好地控制应用程序的响应时间和吞吐量,提供更好的可预测性。
但是CMS还远远达不到完美,主要有四个显著缺点:
1.CMS收集器对CPU资源非常敏感。在并发(并发标记、并发清除)阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。如果在高负载的情况下运行,可能会导致CPU使用率过高。
CMS默认启动的回收线程数是:(CPU数量+3) / 4。收集器线程所占用的CPU数量为:(CPU+3)/4=0.25+3/(4*CPU)。因此这时垃圾收集器始终不会占用少于25%的CPU,因此当进行并发阶段时,虽然用户线程可以跑,但是很缓慢,特别是双核CPU的时候,已经占用了5/8的CPU,吞吐量会很低。为了解决这种情况,产生了“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)。就是采用抢占方式来模拟多任务机制,就是在并发(并发标记、并发清除)阶段,让GC线程、用户线程交替执行,尽量减少GC线程独占CPU,这样垃圾收集过程更长,但是对用户程序影响小一些。实际上i-CMS效果很一般,目前已经被声明为“deprecated”。
2.CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure
错误,失败后而导致另一次Full GC的产生。
由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。
在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数 -XX:CMSInitiatingOccupancyFraction
的值来提高触发百分比,以降低内存回收次数提高性能。JDK1.6中,CMS收集器的启动阈值已经提升到92%。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现 Concurrent Mode Failure
失败,这时候虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction
设置的过高将会很容易导致Concurrent Mode Failure
失败,性能反而降低。
3.CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。
4.需要更多的内存:CMS需要保留一部分内存空间用于垃圾回收过程,因此需要比其他垃圾回收器更多的内存空间。
如果应用程序需要更高的垃圾回收效率和更少的内存碎片问题,可以考虑使用其他垃圾回收器,例如G1。
如何解决内存碎片问题
CMS 使用标记清除发带来了内存碎片问题。为了解决这个问题,CMS收集器提供了一个 -XX:UseCMSCompactAtFullCollection
开关参数,用于在 Full GC 之后增加一个内存碎片的合并整理过程,但是内存整理过程是无法并发的,因此解决了空间碎片问题,却使停顿时间变长,这里使用的是
还可通过- XX:CMSFullGCBeforeCompaction
参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程(默认值是0,表示每次进入Full GC时都进行碎片整理)。
7.G1收集器
G1(Garbage First)收集器是 JDK1.7 提供的一个新的面向服务端应用的垃圾收集器,其目标就是替换掉JDK1.5发布的CMS收集器。G1垃圾收集算法主要应用在多CPU、大内存的服务中,在满足低暂停时间同时,尽可能提高吞吐量,同时处理内存碎片。
其主要优点有:
1.并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短停顿(Stop The World)时间,与用户线程并行执行。
2.分代收集:G1不需要与其他收集器配合就能独立管理整个GC堆,但他能够采用不同方式去处理新建对象和已经存活了一段时间、熬过多次GC的老年代对象以获取更好收集效果。
3.空间整合:从整体来看是基于标记-压缩算法实现,从局部(两个Region之间)来看是基于复制算法实现的,但是都意味着G1运行期间不会产生内存碎片空间,更健康,遇到大对象时,不会因为没有连续空间而进行下一次GC,甚至一次Full GC。
4.可预测的停顿:G1除了追求低停顿,还能建立可预测的停顿模型。因为小分区的特性,它可以以少量时间有优先回收垃圾最多的分区,在不牺牲吞吐量前提下,实现低停顿垃圾回收。可以明确地指定在一个长度为 M 的时间片内,消耗在垃圾收集的时间不超过 N 毫秒。
5.跨代特性,分区特性:之前的收集器进行收集的范围都是整个新生代或老年代,而G1扩展到整个Java堆(包括新生代,老年代),取消了传统的堆空间连续分代机制,而是将堆空间划分为一片片小的分区,但是每一个分区是一个分代。
6.可伸缩性:G1 收集器在设计时考虑到了大内存和多处理器的情况,能够对多个 CPU 和大内存进行优化,因此适用于大型内存和高并发的场景,具有很好的可伸缩性。
综合以上优点,G1 收集器 适用于高性能、大内存的 Java 应用程序。
那么是 G1 收集器怎么实现的呢?
如何实现新生代和老年代全范围收集?
其实它的Java堆布局就不同于其余收集器,它将整个Java堆划分为多个大小相等的独立区域(Region),仍然保留新生代和老年代的概念,可是不是物理隔离的,都是一部分Region(不需要连续)的集合。
如何建立可预测的停顿时间模型?
是因为有了独立区域Region的存在,就避免在Java堆中进行全区域的垃圾收集,G1跟踪各个Region里面的垃圾堆积的价值大小(回收可以获得的空间大小和回收所需要的时间的经验值),后台维护一个优先队列,根据每次允许的收集时间,优先回收价值最大的Region(Garbage-First理念)。因此使用Region划分内存空间以及有优先级的区域回收方式,保证了有限时间获得尽可能高的收集效率。
如何保证垃圾回收只在Region区域进行而不会扩散到全局?
由于Region并不是孤立的,一个Region的对象可以被整个Java堆的任意其余Region的对象所引用,在做可达性判定确定对象是否存活时,仍然会关联到Java堆的任意对象,G1中这种情况特别明显。而以前在别的分代收集里面,新生代规模要比老年代小许多,新生代收集也频繁得多,也会涉及到扫描新生代时也会扫描老年代的情况,相反亦然。
解决:G1收集器Region之间的对象引用以及新生代和老年代之间的对象引用,虚拟机都是使用 Remembered Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(分代的例子中就检查是否老年代对象引用了新生代的对象),如果是则通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中,当进行内存回收时,在GC根节点的枚举范围中加入 Remembered Set 即可避免全堆扫描。
忽略Remembered Set的维护,G1的运行步骤可简单描述为:初始标记(Initial Marking)、并发标记(Concurrenr Marking)、最终标记(Final Marking)、筛选回收(Live Data Counting And Evacution)。
1.初始标记:初始标记仅仅标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新的对象。这阶段需要停顿线程,不可并行执行,但是时间很短。
2.并发标记:此阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,此阶段时间较长可与用户程序并发执行。
3.最终标记:此阶段是为了修正在并发标记期间因为用户线程继续运行而导致标记产生变动的那一份标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这段时间需要停顿线程,但是可并行执行。
4.筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。
如果现有的CMS/ParNew组合没有出现任何问题,没有任何理由去选择G1,如果应用追求低停顿,G1可选择,如果追求吞吐量,和Parallel Scavenge/Parallel Old组合相比G1并没有特别的优势。
最后,欢迎大家提问和交流。
如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!
ChatGPT突然无法登陆?Access denied拒绝访问异常解决方案
2023-02-14
一文掌握,单机Redis、哨兵和Redis Cluster的搭建,建议收藏
2023-02-11
MySQL事务ACID都知道,原理是什么?附面试题
2023-02-07