@

前言

JVM的自动内存管理得益于不断发展的垃圾回收器,从最初的单线程收集到现在并发收集,垃圾回收器的开发者们一直在致力于如何降低GC过程中的停顿时间(STW)以及提高吞吐量,但直到现在也不存在一款完美的垃圾回收器,只能根据不同的场景选择最合适的。所以需要了解每款垃圾回收器出现的背景、原因,并掌握各种垃圾回收器的设计原理、算法实现细节以及各个垃圾回收器的优劣对比,这样才能让我们在调优时做出最合适的选择。这部分内容博主准备分为两篇文章进行总结讲解,本篇主要是对垃圾收集算法的思想以及目前稳定商用的垃圾回收器的讲解。

正文

一、垃圾收集算法

上文分析了JVM判断对象存活的两种算法:引用计数可达性分析。因此垃圾收集算法的实现也对应的分为引用计数式收集追踪式收集,而目前JVM中都没有使用引用计数算法,所以后面讲解的算法都属于追踪式收集。其细分又分为标记-复制标记-清除标记-整理分代回收

标记-复制

复制算法最初的理论是将可用内存分为1:1的两块,每次只使用其中一块,当这块内存满后,就先标记存活对象并将其复制到另一块内存,然后将满的内存释放掉。这种算法非常简单高效,只需要将标记的存活对象复制到另一半空间,同时内存始终保持规整,不会出现内存碎片,但缺点也很明显,可用内存减少了一半,另外复制的对象不能太大,否则复制的效率会比较低。

因为新生代中的对象大多“朝生夕死”,在JVM新生代中的垃圾收集器都是采用的复制算法。但是为避免浪费的空间太多,提出了一种更为优化的复制算法,称为Appel式回收。该算法不再是简单的“半区复制”,而是将新生代分为了三块:一块Eden区和两块Survivor区(分别标记为from和to),默认的分配比例是8:1:1(-XX:SurvivorRatio=8表示两个Survivor区和Eden区比例为2:8,即每个Survivor占10%),每次分配对象都只使用Eden区和其中一块Survivor区(from区)。其中Eden区最大,新对象都在该区域创建,当Eden区满后,会进行一次MinorGC,并将Eden区和from区中存活对象都复制到to区中,然后调换from和to指针。当然肯定是存在to区装不下一次MinorGC存活对象的情况,这时就需要老年代进行分配担保(相关概念在上一篇已经讲过)。

从上面的算法过程中堵着门应该会有一个疑惑:为什么需要两个Survivor区?这里以假设法进行分析。如果没有Survivor区,那么新生代每次GC后存活对象会直接进入老年代,导致老年代迅速填满,频繁的触发FullGC;如果只有一块Survivor区,那么为了保证复制算法的特性(内存规整和高效),Eden区经过一次MinorGC后会将对象复制到Survivor区,这时新对象只能在Survivor区创建,否则无法保证内存规整,但又由于Survivor区非常小,就会导致很快又触发有一次MinorGC;而如果有两块Survivor区就很好的解决了上面所说的问题,而更多的Survivor区就没有必要了。

标记-清除

标记清除是最早出现的垃圾回收算法,由Lisp之父提出。这个算法也很简单,首先标记存活的对象,然后统一回收未被标记的对象。相较于复制算法的缺点也很明显,效率更低,同时会导致内存碎片。为什么效率更低了呢,好比你删除文件,直接格式化文件夹快还是去文件夹中找到文件一个个删除更快?另外内存碎片会导致堆中明明还有足够的内存,但却没有足够的连续内存来存放大对象,导致对象直接进入老年代。

标记-整理

这个算法就是建立在标记清除的基础之上,多了一步整理的工作,标记完成后首先将存活的对象移动到一边,然后清理掉另一边的内存,解决了内存碎片带来的问题。标记-清除标记-整理都适合用在老年代中,而前者相较于后者不用移动内存,而移动内存是一种非常“危险”的操作,需要暂停其它用户线程的执行,确保内存指向的正确性,所以这就是STW出现的原因,就好比你不能在你妈妈打扫屋子的同时边往地上扔垃圾。

分代回收

分代回收严格意义上并不算一种算法,而是各回收算法的实践理论。它建立在两个分代假说之上:

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  • 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消

    亡。

上面两个假说共同确定了垃圾收集器一致的设计原则,即新生代老年代。在新生代中使用复制算法,如上所说,大部分对象朝生夕灭,所以只需要将少量存活对象复制到另一块区域后再统一格式化之前的区域;而老年代因为大量对象存活,只能采用标记清除标记整理算法。

二、常用的垃圾回收器

垃圾回收器是垃圾回收算法的实现,在虚拟机规范中并没有定义要如何实现垃圾回收器,所以各大厂商对垃圾回收器的实现有很大差别,但都是在朝着一个方向努力:低延迟、高吞吐量。

上图中展示的就是目前主流的垃圾回收器,有连线的代表两者可以搭配使用,而打“X”的表示在JDK9中已经废弃的组合,另外从图中我们还可以发现除了G1,其它垃圾回收器都只能作用于新生代老年代中的其中一个区域,那么G1是不是表示废除了分代理论呢?下面来逐个介绍。

Serial/SerialOld

这两个是最早出现的垃圾回收器,如其名,它们都是单线程的垃圾回收器,只适合几十兆到一两百兆的堆空间的垃圾回收,如果用于更大的堆空间会导致系统停顿时间较长,想象一下系统每隔一段时间就要停止处理请求几分钟甚至更长时间,你能接受么?下图是他们的工作原理:



可以看到新生代或老年代在进行垃圾回收时都会暂停所有的用户线程,图中的SafePoint表示线程能够安全暂停的时机,即JVM要进行垃圾回收时,不可能立马就停止所有的线程,那样是非常危险的,必须要确保线程处于安全点才能暂停它。这里先有这个概念,细节在下一篇进行阐述。

该组合可以通过-XX:+UseSerialGC参数开启。

ParNew

该收集器就是Serial的多线程版本,但在单核处理器环境中表现还不如Serial(涉及线程的切换)。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。



另外需要注意的是它是除了Serial之外唯一可以与CMS配合的垃圾收集器,在激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它,在JDK9以后ParNew成为了CMS的一部分。

Parallel Scavenge/ParallelOld

Parallel Scavenge与其它垃圾收集器不同,其它的是追求尽可能小的GC停顿时间,而它主要关注吞吐量,所谓吞吐量就是代码运行时间/(代码运行时间 + 垃圾回收时间)。比如虚拟机运行100分钟,垃圾回收耗时1分钟,那么吞吐量就是99%。但是这款收集器在JDK1.6之前比较尴尬,没有与之对应的并行的老年代收集器,只能采用SerialOld老年代收集器,使得表现比不上PareNew+CMS的组合。直到ParallelOld出现后,Parallel Scavenge才能真正的展现它吞吐量的优势。



Parallel Scavenge有以下几个重要的参数:

  • -XX:MaxGCPauseMillis:该参数的值是一个大于0的毫秒数,收集器尽量保证GC停顿时间不超过该值,但是不要天真的认为该值越小越好。该值设置的太小会导致每次GC的回收率降低,垃圾堆积,GC发生的越来越频繁。比如原先需要100ms收集500M空间,现在设置为50ms,那么可能就只能回收300M或者更小的垃圾。
  • -XX:GCTimeRatio:控制垃圾回收时间比率。比如允许最大垃圾回收时间占总时间的5%,那么需要将该值设置为19(公式是1/(1 + 19))。
  • -XX:+UseAdaptiveSizePolicy:这个参数激活后,就不再需要我们手动设定新生代各区(Eden、from、to)的比例(-XX:SurvivorRatio),晋升老年代对象的大小(-XX:PretenureSizeThreshold),虚拟机会监控运行时的状态,进行动态的调整,这种方式称为垃圾收集的自适应调节策略(GC Ergonomics)。

CMS

CMS(Concurrent Mark Sweep)是第一款并发垃圾收集器,并发是指垃圾收集可以和用户线程同时进行。同时它也是唯一采用标记清除算法对老年代进行回收的垃圾回收器。它包含了以下几个阶段:

  • 初始标记:STW,只标记与GC Roots直接关联的对象
  • 并发标记:和用户线程同时运行,进行可达性分析
  • 重新标记:STW,暂停用户线程,修正上一阶段变动的对象
  • 并发清除:最后是并发的清除掉垃圾



从上面我们可以发现CMS的整个过程中只有初始标记重新标记是需要暂停用户线程的,而初始标记只是标记与GC Roots直接关联的对象,所以耗时只和GC Roots的数量有关,非常快;重新标记的耗时会比初始标记略长,但也远远比并发标记用时短,所以CMS就是通过细分GC的阶段来降低GC的停顿时间。

你可能会好奇为什么需要重新标记并且暂停所有用户线程,因为在与用户线程并发执行的同时肯定会存在引用变动的情况,而要处理这个问题,都是必须要暂停用户线程的,关于引用变动的处理在下一篇会详细分析。

CMS可以说是一款跨时代的垃圾收集器,可以回收几个G到-20G左右的堆空间,但它存在以下几个明显的缺点:

  • CPU敏感:虽然并发标记并发标记是和用户线程并发执行的,但是也因此占用了系统的资源,导致应用程序忽然变慢,降低吞吐量。CMS默认启动的线程数是(处理器核心数+3)/4,因此当核心数量大于等于4时,GC占用资源不超过25%,但核心数小于4时,就会占用大量系统资源。
  • 大量的内存碎片:因为CMS是使用标记清除算法实现垃圾回收,所以会产生大量的内存碎片。为了避免这个问题,CMS采用了一个折中的办法,即提供一个-XX:+UseCMS-CompactAtFullCollection参数,该参数默认开启,控制CMS在进行FullGC的同时进行空间整理,但这样又会导致停顿时间加长,所以还提供了-XX:CMSFullGCsBefore-Compaction参数,控制CMS在进行了多少次不带整理的FullGC后进行一次带整理的FullGC,默认值是0,即每次FullGC都会整理,该参数JDK9后被废弃。
  • 浮动垃圾:因为最终清除的过程也是和用户线程并发执行的,因此这个过程中必然会产生新的垃圾,这一部分垃圾需要预留空间来存放,等待下一次GC的时候再清理,因此会浪费一部分空间。在JDK5的默认配置下,当老年代使用空间超过68%时就会进行GC,到JDK6时,这个阈值就提高到了92%,另外也可以通过-XX:CMSInitiatingOccu-pancyFraction参数控制。但该值越高,那么并发清理过程中可使用的内存就越小,当放不下时,就会出现一次Concurrent Mode Failure,这时候虚拟机就会冻结线程并采用SerialOld进行垃圾回收,导致停顿时间变得更长。

Garbage First

G1是目前最前沿且可商用的垃圾收集器,另外还有ZGC等更为前沿的垃圾收集器还处于试验阶段。它与其它垃圾收集器不同的是,他将堆空间化整为零,将内存区域划分为多个大小相等的独立区域(Region),使得它可以回收堆中的任何一个区域,而不是像其它的垃圾收集器要么只能回收新生代,要么只能回收老年代。但不是说G1就没有新生代和老年代了,它的每个Region都可以根据需要扮演Eden、Survivor或老年代,垃圾收集器也会针对不同角色的Region采用不同的策略去处理。



每个Region的大小可以通过-XX:G1HeapRegionSize设定,取值范围为1M~32M,且必须为2的N次幂。超过单个Region一半容量的对象即为大对象,而对于超过整个Region的对象将会使用多个连续的Humongous空间存放,G1大多数情况下都把Humongous作为老年代一部分看待。



G1的运行过程如上,它也包含了以下4个步骤:

  • 初始标记:STW,也是只标记GC Roots直接关联的对象,并修改TAMS的指针值(G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上,垃圾回收时也不会回收这部分空间),这个过程耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。
  • 并发标记:可达性分析找出要回收的对象,在对象扫描完成后,由于是与用户线程并发执行的,所以存在引用变动的对象,这部分对象会由SATB算法来解决(原始快照,下一篇详细分析)。
  • 最终标记:STW,处理并发阶段遗留的少量遗留的SATB记录。
  • 筛选回收:根据用户设定的-XX:MaxGCPauseMillis最大GC停顿时间对Region进行排序,并回收价值最大的Region,尽量保证满足参数设定的值(该值效果和Parallel Scavenge部分讲解的是一样的)。这里的回收算法就是讲存活的对象复制到空的Region中,即G1局部Region之间采用的是复制算法,而整体上采用的是标记整理算法

G1适合上百G的堆空间回收,与CMS的权衡在6~8G之间,较大的堆内存才能凸显G1的优势,可以通过-XX:+UseG1GC参数开启。

总结

本篇是对常用垃圾收集器的实现原理的整体性分析比较,这一部分是必须掌握的,下一篇则是关于算法的实现细节,如三色标记是什么、并发标记过程中引用变动如何解决、跨代引用如何处理等等一系列问题。

深入探究JVM之垃圾回收器的更多相关文章

  1. 【JVM】垃圾回收器总结(2)——七种垃圾回收器类型

    七种垃圾回收器类型 GC的约定参数 DefNew——Default New Generation Tenured——Serial Old ParNew——Parallel New Generation ...

  2. 深入探究JVM之垃圾回收算法实现细节

    @ 目录 前言 垃圾回收算法实现细节 根节点枚举 安全点 安全区域 记忆集和卡表 写屏障 并发的可达性分析 低延迟GC Shenandoah ZGC 总结 前言 本篇紧接上文,主要讲解垃圾回收算法的实 ...

  3. 深入理解JVM一垃圾回收器

    上一篇我们介绍了常见的垃圾回收算法,不同的算法各有各的优缺点,在JVM中并不是单纯的使用某一种算法进行垃圾回收,而是将不同的垃圾回收算法包装在不同的垃圾回收器当中,用户可以根据自身的需求,使用不同的垃 ...

  4. jvm学习-垃圾回收器(四)

    说明 各种垃圾回收算法都有各自的优缺点.jvm也并没有只采用一种垃圾算法.并提供几种组合供我根据场景进行选择. jvm内存结构 Person p=new Person(); 1.程序里面创建一个对象会 ...

  5. JVM七大垃圾回收器下篇G1(Garbage First)

    G1回收器:区域化分代式 既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First (G1)GC?  原因就在于应用程序所应对的业务越来越庞大.复杂,用户越来越多,没有GC就不能保 ...

  6. JVM七大垃圾回收器上篇Serial、ParNeW、Parallel Scavenge、 Serial Old、 Parallel Old、 CMS、 G1

    GC逻辑分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商.不同版本的JVM来实现. 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本. 从不同角度分析垃圾收 ...

  7. 面试官:说一下JVM常用垃圾回收器的特点、优劣势、使用场景和参数设置

    今天去看牙医,他问我年级轻轻牙齿怎么磨损这么严重?我说,没有人点赞的这些年,我都是咬着牙过来的. Java中的垃圾回收器几乎是面试中的必考点,无论是面试初级,中级还是高级,总免不了要问一问垃圾回收器的 ...

  8. JVM(3) 垃圾回收器与内存分配策略

    文章内容摘自:深入理解java虚拟机 第三章   对象已死? 1. 引用计数算法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0 ...

  9. 第三篇:jvm之垃圾回收器

    一.Serial收集器 新生代收集器,在垃圾回收时,必须暂停其他所有的工作线程.即Stop-The-World. 评价:老而无用,食之无味,弃之可惜. 二.ParNew收集器 新生代收集器,seria ...

随机推荐

  1. 入门大数据---Hbase的SQL中间层_Phoenix

    一.Phoenix简介 Phoenix 是 HBase 的开源 SQL 中间层,它允许你使用标准 JDBC 的方式来操作 HBase 上的数据.在 Phoenix 之前,如果你要访问 HBase,只能 ...

  2. linux下将多个ts文件合并为一个MP4文件

    1. 安装ffmpeg工具 sudo apt install ffmpeg 2. 确保所有ts文件无损坏后,确保当前目录(即存放ts文件的目录)无txt文件及mp4文件,在存放ts文件的目录下建立te ...

  3. dart快速入门教程 (8)

    9.dart中的库 9.1.自定义库 自定义库我们在前面已经使用过了,把某些功能抽取到一个文件,使用的时候通过import引入即可 9.2.系统内置库 以math库为例: import "d ...

  4. 理解C#中的ValueTask

    原文:https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/ 作者:Ste ...

  5. DOM对象增删元素

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  6. html实体引用

    原义字符 等价字符引用 < < > > " " ' &apos; & &

  7. Buy a Ticket,题解

    题目连接 题意: 没个位置有一个点权,每个边有一个边权,求对于每个点u的min(2*d(u,v)+val[v])(v可以等于u) 分析: 我们想这样一个问题,从u到v的边权*2再加一个点权就完了,我们 ...

  8. 洛谷 P3694 邦邦的大合唱站队 状压DP

    题目描述 输入输出样例 输入 #1 复制 12 4 1 3 2 4 2 1 2 3 1 1 3 4 输出 #1 复制 7 说明/提示 分析 首先要注意合唱队排好队之后不一定是按\(1.2.3..... ...

  9. 不用加减乘除做加法(剑指offer-48)

    题目描述 写一个函数,求两个整数之和,要求在函数体内不得使用+.-.*./四则运算符号. 题目解析 首先看十进制是如何做的: 5+7=12,三步走 第一步:相加各位的值,不算进位,得到2. 第二步:计 ...

  10. springbean 生命周期

    springbean 和java对象得区别: 1.对象:任何符合java语法规则实例化出来的对象 2.springbean: 是spring对普通对象进行了封装为BeanDefinition,bean ...