写点什么

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

用户头像
Silently9527
关注
发布于: 2021 年 01 月 27 日
面试官常问的垃圾回收器,这次全搞懂

本文已被 Github 仓库收录 https://github.com/silently9527/JavaCore

微信公众号:贝塔学 Java

前言

前几天写了一篇《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 ,回复源码即可



发布于: 2021 年 01 月 27 日阅读数: 27
用户头像

Silently9527

关注

公众号:贝塔学JAVA 2018.05.09 加入

Simple Programmer, Make the complex simple

评论

发布
暂无评论
面试官常问的垃圾回收器,这次全搞懂