写点什么

垃圾回收器的前世今生

用户头像
中原银行
关注
发布于: 16 小时前
垃圾回收器的前世今生

为什么会出现回收垃圾

  之前在工作中遇到了服务半夜下线,通过 grafana 监控数据分析,推断是发生了 Full GC,导致应用线程中断时间过长,以至于注册中心对应用的心跳检测失败,将该服务从注册中心剔除掉。

  那什么是 Full GC?首先要说明 GC 的含义:Garbage Collection,垃圾回收。Full GC 就是对内存中大部分区域进行垃圾回收,此时用于垃圾回收的线程会独占 CPU,因此应用程序会被挂起,无法运行。

 那就有三个问题:垃圾是谁?从哪来?到哪去?

 先上两行代码。代码运行所在的内存相当于一个屋子,a 相当于一个柜子,这个柜子本来装着衣服,后来把衣服换成了鞋子,但此时衣服还在屋子里,之后再没穿过,它就是垃圾了,代码运行到括号外面了,那这个柜子也成垃圾了,因为用不到了。把这些垃圾清理了,拿回空间就能放其他用的上的东西了。

{    String a = "衣服";	a = "鞋子";}
复制代码

垃圾就是程序使用后,不再使用、又无法被再次利用的空间;垃圾由应用程序产生,被清理后对应空间可再被利用。

垃圾回收器的前世

  最初还没有 GC,程序员需要自己手动进行内存管理,申请内存,存放对象并没太大问题,但释放所有不再使用的内存空间,难度就会很大:如果忘记释放,会发生内存泄露,内存堆满后可能会导致系统崩溃;而如果错误释放了正在使用的空间,程序就会出现 bug。

  因此技术大佬们就研究设计了 GC,让计算机来自动管理内存,程序员只需专注在业务编码上。但过度依赖于“自动”,就会弱化开发人员在程序出现内存溢出时定位问题和解决问题的能力。因此了解垃圾回收原理能够提升解决内存相关问题的能力,以及性能调优的能力

垃圾回收算法

本节简单介绍下垃圾回收算法的实现原理,作为后文介绍垃圾回收器的铺垫。

1960 年出现第一个 GC 算法:标记—清除算法。这个算法会首先标记出所有需要回收的对象,标记完成后统一回收。缺点是会产生内存碎片。


  1963 年出现复制算法。此算法将可用内存分为两块,每次只使用一块。当这块的内存用完,将还存活的对象复制到另一块,然后清理使用过的内存空间。复制算法不会有内存碎片的问题,但该算法的缺点是只有一半内存可用,内存浪费严重,而且如果垃圾较少,需复制的对象较多,则耗时较长。


  在这之后研究的许多算法可以说都是基于上述两个算法,进行组合或应用而实现的。例如标记—整理算法:组合标记—清除算法与复制算法,标记出所有需要回收的对象,让所有存活的对象都聚到一端,然后清理端边界以外的内存。解决内存碎片以及内存浪费的问题,但缺点是算法复杂,需考虑对象引用问题。


  每种算法都有其优劣点,最好的方法是适合的场景用适合的算法。因此提出了分代垃圾回收这个策略,导入了年龄的概念,将对象分类成新生代老年代,针对不同的代使用不同的 GC 算法。新生代中存放刚生成的对象,这些对象大部分生命周期较短,适合复制算法,需复制的对象不多,速度快并且无内存碎片问题。在经过了一定次数的新生代 GC 的对象被当作老年代对象,这种情况称作晋升(promotion)。老年代对象生命周期长,不适合复制算法,更适合标记-清除和标记-整理算法。


 分代回收仅考虑了对象的生命周期,无法控制 GC 回收的大小。因此分区回收算法应时而生,将整个堆划分成连续的小区间,每个小区间独立使用和回收,GC 时可以控制要回收区间的数量,从而控制 GC 产生的停顿时间,分区间采用复制算法,效率更高。


垃圾回收器的今生

评价指标

  有了垃圾回收算法,对应的程序实现就是垃圾回收器。但即便基于相同的算法原理,回收器也会有不一样的实现,呈现出来的性能效果也各有倾向

 而考量一个垃圾回收器是否适用主要有两个指标:吞吐量最大暂停时间

吞吐量

        负责 GC 的线程会和应用程序线程争用当前可用 CPU 的时钟周期。应用程序线程用时占程序总用时的比例即为吞吐量。吞吐量越高越好。

• 最大暂停时间

       GC 时会出现应用程序线程挂起,仅 GC 线程运行的场景。出现此现象最长的一次的时间就是最大暂停时间。暂停时间越短越好

  但吞吐量和最大暂停时间是相互矛盾的,吞吐量大意味着 GC 次数少,而 GC 少则意味着一次 GC 会有更多的垃圾需要处理,暂停时间就会变长。因此若是交互式的应用,注重用户体验,则选择暂停时间更短的垃圾回收器;若应用追求短时间完成任务,则选择吞吐量更高的回收器。

今生发展

  回收器的发展可以分成三个阶段:串行并行并发。同时从算法层面又主要分为分代收集分区收集

  串行回收器简单高效,无线程切换的开销,适合单核环境、内存较小的场景。主要实现由 Serial 和 Serial Old 回收器,采用分代收集。

Serial:jdk1.3 之前新生代回收的唯一选择,使用复制算法,单线程独占 CPU 进行回收。

Serial Old:老年代版的 Serial使用标记-整理算法,也是 CMS 收集器的后备收集器。

但如今计算机内存至少 G 级别,并行计算能力也更强,因此在串行回收器上进一步改进,实现并行回收器,仍然独占 CPU,但多线程处理速度更快,暂停时间更短。主要实现有 ParNew、Parallel Scavenge 和 Parallel Old,同样采用分代收集。

• ParNew: 用于新生代收集,相当于多线程版本的 Serial 回收器,使用复制算法,常与 CMS 组合使用。

• Parallel Scavenge:jdk1.4 正式发布,jdk1.6 时作为默认新生代回收器。使用复制算法,作为新生代回收器。不同在于 ParNew 追求低停顿时间,Parallel Scavenge 追求高吞吐量,短时间完成任务,但不适合交互式应用。

• Parallel Old:jdk1.6 正式使用,jdk1.7 作为默认老年代回收器。Parallel Scavenge 的老年代版本,使用标记-整理算法,追求高吞吐量。

并行回收器虽然采用多线程的方法缩短了暂停时间,但整个 GC 过程仍然是独占 CPU 的,进一步改进,采用多线程并发回收。并发回收器的回收流程中仅小部分环节是独占 CPU,会产生暂停时间,其余环节则与应用线程并发执行,进一步缩短暂停时间。CMS G1 是当前应用最广泛的并发回收器。

CMS: 全称:Concurrent Mark Sweep。jdk1.4 正式发布,作为老年代回收器。使用标记—清除算法,并使用分代收集,默认 ParNew 收集器作为新生代收集器。

            CMS 是里程碑式的回收器,开启了并发回收的时代,但它从未被作为默认回收器,并且在 JDK9 被标记弃用,JDK14 被删除。

            原因就在于 CMS 有许多问题,包括吞吐量低、产生浮动垃圾、产生内存碎片。尤其 G1 回收器的发布,便替代了 CMS。

• G1:全称:Garbage First。jdk1.7 正式发布,jdk9 作为默认收集器,工作在新生代以及老年代。G1 使用分区算法。

            G1 基于分区,能够更细粒度的回收,并且在区域间基于复制算法来回收,从而实现压缩。因此相比 CMS 内存碎片少很多。可以指定预期停顿时间,收集器会根据预期目标,只回收价值大(垃圾多)的区域,而 CMS 需要扫描整个老年代进行回收。

            

实践对比

  回到文章开头提到的服务下线的问题,可以考虑 G1 来替换 CMS 来提升性能,但口说无凭,实践对比。

  首先是 JVM 参数配置,根据服务器的内存来分配,测试机内存 8G,考虑其他应用占用,JVM 配置 4G。

//统一配置参数-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xmx4g -Xms4g -Xloggc:gc.log -XX:MetaspaceSize=1g -XX:MaxTenuringThreshold=1
//CMS参数-Xmn200m -XX:SurvivorRatio=7 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark -XX:CMSFullGCsBeforeCompaction=3 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
//G1参数-XX:+UseG1GC -XX:+DisableExplicitGC
复制代码

从配置参数上就能看出明显差异,CMS 需要配置多达 9 个参数,G1 因为具有自适应的特性,则只需配置个参数,极大减轻开发人员学习成本,将更多精力放在功能开发上。

  基于同一段代码,测试 CMS 与 G1,代码中会不断生成小对象,并间断的生成 4M 大对象。CMS 与 G1 的表现如下图所示。



  从 CPU 占用率以及停顿时间方面对比,G1 表现都要更优秀。后续又对 CMS 进行简单调优后,较好的表现是新生代收集 45 次,耗时 7.74 秒,老年代收集 8 次,耗时 0.74 秒。相比之前性能好了许多,但相比 G1 仍然要差。

  因此如果是新系统,或者系统使用 CMS 后出现过内存回收引起的溢出等问题,推荐使用 G1 回收器。但 G1 更适合大内存的服务使用,如果服务器内存较小,应用内存吃紧,始终需进行整个堆的回收,G1 的工作量并不少,甚至因为算法更复杂,性能更差,不推荐改用 G1。

垃圾回收器的未来

  前文提到垃圾回收器的评价指标主要为吞吐量与最大暂停时间。未来垃圾回收器的发展趋势主要是缩短暂停时间,这其实也与当前软件更注重用户体验的趋势一致。

• ZGC:jdk11 发布。基于分区,采用复制算法,并进行改进,改进后的算法基本全程并发运行。今年 3 月份正式发布的 jdk16 里对 ZGC 进一步优化。

           暂停时间达到 O(1) 级,并且不随着内存大小而增大。即使是 TB 级的内存,平均暂停时间也约为 0.05 毫秒,最大暂停时间则约为 0.5 毫秒。更适合大内存低延迟的服务进行内存回收。

• shenandoah:jdk12 发布的。基于分区,采用复制算法,并进行改进。暂停时间比 ZGC 要长,在 10 毫秒左右,但吞吐量比 ZGC 更高,暂停时间同样与内存大小无关。

还有其他的一些收集器,比如 Azul 的 Zing JVM 使用的 C4(Concurrent Continuously Compacting Collector)收集器 、用于分布式垃圾回收的 DGC。篇幅原因,不再一一介绍。

垃圾回收的内容有非常多,本文只是引子。各位同学,有兴趣的话,可以继续深耕。

参考

•           [1]. 深入Java虚拟机:JVM G1GC的算法与实现 中村成洋

•           [2]. 《垃圾回收的算法与实现》 中村成洋 / 相川光

•           [3]. 《深入理解Java虚拟机》 周志明

•           [4]. Java中9种常见的CMS GC问题分析与解决 美团

•           [5]. Garbage-First Garbage Collection David Detlefs / Christine Flood

•           [6]. Major GC和Full GC的区别 RednaxelaFX

•           [7]. G1: One Garbage Collector To Rule Them All Monica Beckwith

•           [8]. ZGC | What's new in JDK 16 perliden

•           [9]. OpenJDK16

用户头像

中原银行

关注

打造科技驱动、创新引领的数字化未来银行。 2020.02.06 加入

中原银行是河南省属法人银行,总部位于河南省郑州市。我行坚持“科技立行、科技兴行”,秉承“稳健 创新 进取 高效”理念,发展移动金融、线上金融,提升综合金融服务能力,金融科技应用水平居国内城商行领先地位。

评论

发布
暂无评论
垃圾回收器的前世今生