Java教程

面试官常问的垃圾回收器,这次全搞懂

本文主要是介绍面试官常问的垃圾回收器,这次全搞懂,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
本文已被Github仓库收录 https://github.com/silently9527/JavaCore

前言

前几天写了一篇《JVM性能调优实战:让你的IntelliJ Idea纵享丝滑》,其中有对GC垃圾回收器的选择尝试,本篇我们就来详细的看看JVM中常见的垃圾回收器有哪些以及每个垃圾回收器的特点,这也是面试的时候经常被问的内容

JVM堆内存概览

在聊垃圾回收器之前,我们先来看看JVM堆内存的区域划分是怎么样的,看下图

  • 因为虚拟机使用的垃圾回收算法是分代收集算法,所以堆内存被分为了新生代和老年代
  • 新生代使用的垃圾回收算法是复制算法,所以新生代又被分为了 Eden 和Survivor;空间大小比例默认为8:2
  • Survivor又被分为了S0、S1,这两个的空间大小比例为1:1

内存分配以及垃圾回收

  1. 对象优先在Eden区进行分配,如果Eden区满了之后会触发一次Minor GC
  2. Minor GC之后从Eden存活下来的对象将会被移动到S0区域,当S0内存满了之后又会被触发一次Minor GC,S0区存活下来的对象会被移动到S1区,S0区空闲;S1满了之后在Minor GC,存活下来的再次移动到S0区,S1区空闲,这样反反复复GC,每GC一次,对象的年龄就涨一岁,默认达到15岁之后就会进入老年代,对于晋身到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold设置
  3. 在Minor GC之后需要的发送晋身到老年代的对象没有空间安置,那么就会触发Full GC (这步非绝对,视垃圾回收器决定)
Minor GC和Full GC的区别:Minor GC是指发生在新生代的垃圾收集行为,由于对象优先在Eden区分配,并且很多对象都是朝生夕死,所以触发的频率相对较高;由于采用的复制算法,所以一般回收速度非常快。Full GC是指发生在老年代的垃圾收集行为,Full GC的速度一般会比Minor GC慢10倍以上;所以不能让JVM频繁的发生Full GC

为了能够更好的适应不同程序的内存情况,JVM也不一定要求必须达到年龄15岁才能晋身到老年代,如果在Survivor区中相同年龄的所有对象大小总和大于Survivor区空间的一半,年龄大于或者等于这个年龄的对象将会直接进入到老年代

Full GC触发条件

  • 代码中调用System.gc()
  • 老年代空间不足/满了
  • 持久区空间不足/满了
注意:大对象会直接在老年代分配内存,可以通过参数-XX:PretenureSizeThreshold控制对象的大小,通常遇到的大对象是很长的字符串或者数组,如果分配了一大群大对象只是临时使用,生命很短暂,那么就会频繁的发生Full GC,但是此时的新生代的空间还有空闲;写代码的时候,这种情况应该避免,特别是在创建数组的时候要当心

空间担保

在新生代发生Minor GC的时候,JVM会先检查老年代中可分配的连续空间是否大于新生代所有对象的总和,如果大于,那么本次Minor GC就可以安全的执行;如果不大于,那么JVM会先去检查参数HandlePromotionFailure设置值是否允许空间担保失败,如果允许,JVM会继续检查老年代可分配的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尽管这次Minor GC是有风险的,JVM也会尝试一次Minor GC;如果不允许担保失败,那么JVM直接进行Full GC

虽然担保有可能会失败,导致饶一圈才能进行GC,但是还是建议把这个参数打开,可以避免JVM频繁的Full GC

垃圾回收器概览

从上图可以看出:

  • 新生代可以使用的垃圾回收器:Serial、ParNew、Parallel Scavenge
  • 老年代可以适用的垃圾回收器:CMS、Serial Old、Parallel Old
  • G1回收器适用于新生代和老年代
  • 相互之间有连线的表示可以配合使用
CMS和Serial Old同为老年代回收器,为何相互会有连线呢?

Serial收集器

这是个单线程收集器,发展历史最悠久的收集器,当它在进行垃圾收集工作的时候,其他线程都必须暂停直到垃圾收集结束(Stop The World)。

虽然Serial收集器存在Stop The World的问题,但是在并行能力较弱的单CPU环境下往往表现优于其他收集器;因为它简单而高效,没有多余的线程交互开销;Serial对于运行在Client模式下的虚拟机来说是个很好的选择

使用-XX:+UseSerialGC参数可以设置新生代使用这个Serial收集器

ParNew收集器

ParNew收集器是Serial收集器的多线程版本;除了使用了多线程进行垃圾收集以外,其他的都和Serial一致;它默认开始的线程数与CPU的核数相同,可以通过参数-XX:ParallelGCThreads来设置线程数。

从上面的图可以看出,能够与CMS配合使用的收集器,除了Serial以外,就只剩下ParNew,所以ParNew通常是运行在Server模式下的首选新生代垃圾收集器

使用-XX:+UseParNewGC参数可以设置新生代使用这个并行回收器

Parallel Scavenge收集器

Parallel Scavenge收集器依然是个采用复制算法的多线程新生代收集器,它与其他的收集器的不同之处在于它主要关心的是吞吐量,而其他的收集器关注的是尽可能的减少用户线程的等待时间(缩短Stop The World的时间)。吞吐量=用户线程执行时间/(用户线程执行时间+垃圾收集时间),虚拟机总共运行100分钟,其中垃圾收集花费时间1分钟,那么吞吐量就是 99%

停顿时间越短适合需要和用户进行交互的程序,良好的响应能够提升用户的体验。而高效的吞吐量可以充分的利用CPU时间,尽快的完成计算任务,所以Parallel Scavenge收集器适用于后台计算型任务程序。

-XX:MaxGCPauseMillis可以控制垃圾收集的最大暂停时间,需要注意不要以为把这个时间设置的很小就可以减少垃圾收集暂用的时间,这可能会导致发生频繁的GC,反而降低了吞吐量

-XX:GCTimeRatio设置吞吐量大小,参数是取值范围0-100的整数,也就是垃圾收集占用的时间,默认是99,那么垃圾收集占用的最大时间 1%

-XX:+UseAdaptiveSizePolicy 如果打开这个参数,就不需要用户手动的控制新生代大小,晋升老年代年龄等参数,JVM会开启GC自适应调节策略

Serial Old收集器

Serial Old收集器也是个单线程收集器,适用于老年代,使用的是标记-整理算法,可以配合Serial收集器在Client模式下使用。

它可以作为CMS收集器的后备预案,如果CMS出现Concurrent Mode Failure,则SerialOld将作为后备收集器。(后面CMS详细说明)

Parallel Old收集器

Parallel Old收集器可以配合Parallel Scavenge收集器一起使用达到“吞吐量优先”,它主要是针对老年代的收集器,使用的是标记-整理算法。在注重吞吐量的任务中可以优先考虑使用这个组合

-XX:+UseParallelOldGc设置老年代使用该回收器。

XX:+ParallelGCThreads设置垃圾收集时的线程数量。

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,在互联网网站、B/S架构的中常用的收集器就是CMS,因为系统停顿的时间最短,给用户带来较好的体验。

-XX:+UseConcMarkSweepGC设置老年代使用该回收器。

-XX:ConcGCThreads设置并发线程数量。

CMS采用的是标记-清除算法,主要分为了4个步骤:

  • 初始化标记
  • 并发标记
  • 重新标记
  • 并发清除

初始化标记和重新标记这两个步骤依然会发生Stop The World,初始化标记只是标记GC Root能够直接关联到的对象,速度较快,并发标记能够和用户线程并发执行;重新标记是为了修正在并发标记的过程中用户线程产生的垃圾,这个时间比初始化标记稍长,比并发标记短很多。整个过程请看下图

优点

  • CMS是一款优秀的收集器,它的主要优点:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  • CMS收集器对CPU资源非常敏感。 在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾。 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,回收阀值可以通过参数-XX:CMSInitiatingoccupancyFraction来设置;如果回收阀值设置的太大,在CMS运行期间如果分配大的对象找不到足够的空间就会出现“Concurrent Mode Failure”失败,这时候会临时启动SerialOld GC来重新进行老年代的收集,这样的话停顿的时间就会加长。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。为了解决这个问题CMS提供了一个参数-XX:+UseCMSCompactAtFullCollecion,如果启用,在Full GC的时候开启内存碎片整理合并过程,由于内存碎片整理的过程无法并行执行,所以停顿的时间会加长。考虑到每次FullGC都要进行内存碎片合并不是很合适,所以CMS又提供了另一个参数-XX:CMSFullGCsBeforeCompaction来控制执行多少次不带碎片整理的FullGC之后,来一次带碎片整理GC

G1收集器

G1是一款面向服务端应用的垃圾回收器。

  • 并行与并发:与CMS类似,充分里用多核CPU的优势,G1仍然可以不暂停用户线程执行垃圾收集工作
  • 分代收集:分代的概念依然在G1保留,当时它不需要和其他垃圾收集器配合使用,可以独立管理整个堆内存
  • 空间的整合:G1整体上采用的是标记-整理算法,从局部(Region)采用的是复制算法,这两种算法都意味着G1不需要进行内存碎片整理
  • 可预测的停顿:能够让用户指定在时间片段内,消耗在垃圾收集的时间不超过多长时间。

Region

虽然在G1中依然保留了新生代和老年代的概念,但是采用的是一种完全不同的方式来组织堆内存,它把整个堆内存分割成了很多大小相同的区域(Region),并且新生代和老年代在物理上也不是连续的内存区域,请看下图:

每个Region被标记了E、S、O和H,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象,当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。Region区域的内存大小可以通过-XX:G1HeapRegionSize参数指定,大小区间只能是2的幂次方,如:1M、2M、4M、8M

G1的GC模式

  • 新生代GC:与其他新生代收集器类似,对象优先在eden region分配,如果eden region内存不足就会触发新生代的GC,把存活的对象安置在survivor region,或者晋升到old region
  • 混合GC:当越来越多的对象晋升到了old region,当老年代的内存使用率达到某个阈值就会触发混合GC,可以通过参数-XX:InitiatingHeapOccupancyPercent设置阈值百分比,此参数与CMS中-XX:CMSInitiatingoccupancyFraction的功能类似;混合GC会回收新生代和部分老年代内存,注意是部分老年代而不是全部老年代;G1会跟踪每个Region中的垃圾回收价值,在用户指定的垃圾收集时间内优先回收价值最大的region
  • Full GC:如果对象内存分配速度过快,混合GC还未回收完成,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,此过程与CMS类似,会导致异常长时间的暂停时间,尽可能的避免full gc.
    • -

写到最后(点关注,不迷路)

文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。

最后,请朋友们不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏

    • -
我已经从零开始手写了简易版springmvc,以及编写了详细的说明文档,希望能够帮助伙伴们深入理解springmvc核心原理,有需要的朋友欢迎关注公众号:贝塔学JAVA ,回复源码即可

image

这篇关于面试官常问的垃圾回收器,这次全搞懂的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!