垃圾回收算法介绍与JVM垃圾回收器选择指北

许多高级编程语言都带有自动垃圾回收特性,以将程序员从繁琐复杂的内存分配和释放工作中解脱。本文将概述常见的垃圾回收算法,并详细介绍JVM众多垃圾回收器的特性、区别和选择指北。

垃圾回收算法

垃圾回收算法本质上是“识别已不会再被使用的内存数据并将其清除释放”的算法。其中“识别”这一阶段,主要有两种方式:

引用计数法

统计每个存活对象的引用次数,引用次数为0的对象视为待回收的垃圾对象。
引用计数法的弊端是无法回收循环引用的对象。如A引用B,B引用A,同时A和B都没有其他引用,此时A和B应该都是待回收的垃圾对象,但引用计数法无法识别。

可达性分析法

从GC Roots开始遍历内存池中的所有对象,任何能够从GC Roots直接或间接引用到的对象便被标记为“可达”,待遍历完成后,所有未被标记为“可达”的对象即为待回收的垃圾对象

GC Roots:
垃圾回收器用于进行可达性分析的根对象,GC Roots通常会有多个,以Java为例,一个运行中的JVM,其GC Roots可能包括:
Class - 由系统类加载器(system class loader)加载的对象
Thread - 所有活着的线程
Stack Local - Java方法的local变量或参数
JNI Local - JNI方法的local变量或参数
JNI Global - 全局JNI引用
Monitor Used - 用于同步的监控对象
Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等

由于引用计数法的弊端,已几乎不再被现代的编程语言使用。而可达性分析法则得到了广泛使用,几乎是所有垃圾回收算法标配的垃圾识别方式。

接下来,详细介绍一下常用的垃圾回收算法:

标记-清除算法(Mark-Sweep)

也被称为Mark-Clean算法
标记阶段:从GC Roots开始进行可达性分析,将所有可达对象标记出来
清除阶段:将所有未标记可达的对象清理掉
标记-清除算法带来的问题是内存碎片,被清除掉的对象会在内存中留下很多的“洞”,这些“洞”的大小可能不足以容下新创建的对象,随着碎片的增多,内存池中实际可用的内存可能会变得越来越少

拷贝算法

  • 将内存划分为两个区域,一次只使用其中一个(暂称为from区)
  • 在进行可达性分析的同时,将可达对象复制到另一个空白的内存区域中(暂称为to区)
  • 可达性分析完成后,将to区与from区的身份互换,同时将原先的from区(现在的to区)中的对象清除

拷贝算法可以解决内存碎片的问题,因为向to区中复制的对象使用的都是连续的内存空间。同时拷贝算法的效率要比标记-清除算法高很多(尤其是在存活对象少、垃圾对象多的场景下),但代价是占用了冗余的内存空间。

标记-压缩算法(Mark-Sweep-Compact)

也被称为标记-整理算法、Mark-Compact算法、MSC算法
与标记-清除算法类似,区别在于在标记阶段完成后,将所有可达对象移动到内存池的另一端,形成一整块连续的内存空间,然后再将范围外的内存池清空。
标记-压缩算法的好处是不会产生内存碎片,也不会占用冗余的内存空间,但代价是需要付出额外的工作将存活对象移动,效率低于标记-清除算法。

增量回收(Incremental Collecting)

增量回收与上述的垃圾回收算法不是相互替代的关系,可以将其理解为一种标准垃圾回收算法的使用方式。
在垃圾回收可达性分析的过程中,为了避免分析过程中产生新的对象影响分析的准确性,通常会将进程置于STW(Stop-The-World)状态,即在垃圾回收时挂起所有线程,暂停除垃圾回收外的一切工作。进程占用的内存空间越大,待回收的垃圾对象越多,STW持续的时间就越长。
增量回收的基本思想是每次只收集一小片区域的内存空间,然后恢复其他线程的工作,依次反复,直到垃圾收集完成。使用这种方式能够避免长时间的STW。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分代回收(Generational Collecting)

不同的垃圾回收算法有不同的优势和代价,简单地选用其中一种无法应对可能出现的各种场景。分代回收的思想是将内存池按特点划分为多个区域,针对每个区域的特点应用不同的垃圾回收算法。
以Hotspot VM为例,将堆内存划分为新生代(Young Generation)和老年代(Old Generation)两个区域。新生代中存储新诞生的对象,老年代中存储长时间存活的对象。这样可以让新生代始终处于存活对象少、垃圾对象多的状态,需要比较频繁的进行垃圾回收,适合使用拷贝算法;而老年代则始终处于存活对象多,垃圾对象少的状态,不需要频繁进行垃圾回收,适合使用标记-压缩算法。

JVM垃圾回收器

本节将以HotSpot VM为例,介绍JVM中各种垃圾回收器

Serial

使用拷贝算法的垃圾回收器,只能用于新生代。Serial回收器使用单线程进行垃圾回收。

SerialOld

使用标记-压缩算法的垃圾回收器,只能用于老年代。使用单线程进行垃圾回收。

Serial+SerialOld回收器的组合是JVM最基础的垃圾回收器组合,只使用单CPU,STW时间较长,适用于处理能力不强的主机和对STW时长要求不高的软件,JVM如以client模式启动,则默认使用Serial+SerialOld回收器。
在启动参数中指定-XX:+UseSerialGC,会启用Serial+SerialOld组合

Serial和SerialOld回收器适用于只有单核CPU的运行环境,效率比较低,一般情况下,只用于开发环境或桌面程序

ParNew

Serial回收器的多线程版本,只能用于新生代。使用拷贝算法,多线程并行工作。在多CPU主机上的性能高于Serial,单CPU主机上的性能低于Serial。

在启动参数中指定-XX:+UseParNewGC,会使用ParNew+SerialOld的回收器组合

Parallel Scavenge

与ParNew一样,都是用于新生代的并行拷贝算法回收器。区别在于Parallel Scavenge回收器可以控制新生代垃圾回收的STW时间。
可以通过参数-XX:MaxGCPauseMillis设置最大的STW时间,这样Parallel Scavenge回收器就会通过增加或减少回收次数的方式把每次STW时长尽可能控制在指定的范围内。还可以通过-XX:GCTimeRatio参数指定用于垃圾回收的时间最大占比。
在使用Parallel Scavenge回收器时,最好通过参数-XX:+UseAdaptiveSizePolicy开启回收器的自适应策略,开启后JVM可以根据实际的情况动态调整新生代中Eden、S0、S1三个分区的大小比例,以及晋升老年代对象的年龄等参数,以满足指定的最大STW市场和最大垃圾回收时间占比要求。

在启动参数中指定-XX:+UseParallelGC,会使用Parallel Scavenge+SerialOld的回收器组合。(Hotspot VM中,此时老年代的回收器被称为PS MarkSweep,其实本质上就是SerialOld,具体可见R大对此的说明:http://hllvm.group.iteye.com/group/topic/27629

Parallel Old

Parallel Scavenge的老年代版本,于JDK1.6中面世。在Parallel Old诞生之前,Parallel Scavenge处于一个比较尴尬的境地,由于没有对应的老年代回收器与其配合,仅在新生代使用Parallel算法很难达到对整体垃圾回收时长和STW时长的控制目的。
而现在,我们可以使用Parallel Scavenge+Parallel Old这一组合来解决这一问题

在启动参数中指定-XX:+UseParallelOldGC,会使用Parallel Scavenge+Parallel Old的回收器组合

作为服务端的JAVA程序,使用Parallel Scavenge+Parallel Old的回收器组合算是基础标配了,这两款回收器的组合能够有效地利用服务器的多CPU配置,提高垃圾回收效率。并且足够简单明了,不怎么需要进行精细的参数调优。

然而,如果应用程序对STW时长有非常严格的要求,那么HotSpot VM还提供了更强大的垃圾回收器:

Concurrent Mark Sweep(CMS)

CMS是标记-清除的改进算法,用于老年代,能够有效减少STW时长。
CMS是一种比较复杂的垃圾回收算法,此处尽可能进行简明扼要的介绍:
CMS将标记-清除细分为6个阶段:

  • 初始标记
  • 并发标记
  • 并发预清理
  • 重新标记
  • 并发清理
  • 并发重置

详细过程:

  1. 初始标记阶段中,CMS回收器标记出被GC Roots直接引用的对象,这一过程需要STW,但由于只进行深度为1的遍历,耗时很短。
  2. 并发标记阶段中,CMS回收器以初始标记阶段标记出的存活对象为根进行可达性遍历。在这一阶段中,不需要STW,其他线程可正常运行。
  3. 并发预清理阶段中,CMS回收器对并发标记阶段中老年代新增的对象重新进行标记。这一阶段存在的目的是尽可能减少下一阶段“重新标记”的STW时长。
  4. 重新标记阶段会进入STW,然后进行一次完整的可达性分析,由于前面三个阶段已经完成了绝大部分的工作,所以这一阶段的STW会很短。
  5. 并发清理阶段不需要STW,垃圾回收线程清理标记出的垃圾对象,同时其他线程可以正常工作。
  6. 并发重置阶段中,重置CMS回收器的数据结构,等待下一次垃圾回收。

可以看出来,CMS回收器的思路是把标记-清除算法的工作拆分成多个步骤,其中可以并行的尽可能并行,以达到STW时长最小化的目标。
同样地,CMS回收器也存在着弊端:

  • 对CPU要求高,由于部分标记和清除阶段是免STW并且多线程并行的,这就给CPU增加了很大的线程切换压力,核数少的CPU使用CMS回收器的效果并不好,多核多CPU的高性能主机更加适合使用
  • 垃圾回收的同时老年代中仍然在产生新的对象,这是CMS的并行机制导致的。所以CMS回收器不能在老年代满时才开始工作,Hotspot VM 6/7中,CMS回收器在老年代使用率92%时便开始工作。如果在CMS垃圾回收的过程中,新增的对象占满了剩余的8%空间,便会导致CMS回收失败,自动降级至SerialOld重新进行垃圾回收。(CMS垃圾回收触发的时机可以使用-XX:CMSInitiatingOccupancyFraction参数进行设置)
  • CMS使用的是标记-清除算法,而不是标记-压缩算法,这就导致会出现大量的内存碎片。随着内存碎片的增多,最终势必会出现垃圾回收的并发阶段中内存不足的情况,如上所说,此时CMS回收器会自动降级为SerialOld回收器,以标记-压缩算法进行垃圾回收,同时也就会整理好内存碎片。

综上所述可以看出,CMS回收器的机制比前述的任何一种都要精细和复杂,同时对CPU资源的要求也要高出很多,以牺牲性能的代价换取最少的STW时长。

在启动参数中指定-XX:+UseConcMarkSweepGC,会开启ParNew+CMS的回收器组合。

CMS回收器在控制STW时长的表现上要比Parallel Old好很多,然而代价是占用更多的计算资源,如果服务器的计算资源有冗余,使用CMS是更好的选择。

Garbage-First(G1)

G1回收器诞生于Hotspot VM的7update4版本,这一最新型的垃圾回收器吸取了CMS回收器的经验和教训,旨在解决CMS回收器的各类弊端,同时提供更短更可控的STW时长。

G1的机制比CMS更加复杂,此处同样尽可能简明的进行介绍:

与其他回收器不同,G1是一个全代回收器,同时负责新生代和老年代的垃圾回收工作。
G1回收器打破了Hotspot VM以往的分代概念,新生代的Eden、S0、S1,以及老年代不再是物理分隔,而变成了灵活的逻辑分隔。G1将堆内存划分为2000个左右相等的内存块,每个内存块的大小为1-32Mb。每个内存块可以作为Eden、S0、S1或老年代使用,也就是说这些块的身份是不固定的。随着每次垃圾回收的完成,有些块的内存会被完全释放掉,成为空白块,而这些空白块在接下来可能成为任何一种角色。

图片.png

G1在进行Young GC(对新生代进行的垃圾回收)时,会触发STW,并行地将所有Eden块和Survivor From块中的存活对象拷贝至一或多个Survivor To块中,然后将Eden块和Survivor From块清除成为空白块。接下来,G1会根据各种信息动态地计算并重新分配接下来Eden区和Survivor区应占的内存空间大小。

G1在进行Old GC(对老年代进行的垃圾回收)时,会采用类似于CMS回收器的多个阶段进行:

  1. 初始标记:标记出对老年代对象有引用的内存块,需要STW
  2. 并行标记:针对初始标记出的内存块进行可达性分析,并计算每个内存块的活跃度(可以理解为该块中的可达对象占比)。如果在这一环节中发现某个内存块中所有的对象都不可达,则将该块标记出来。
  3. 重新标记:将并行标记阶段中发现的整体不可达内存块彻底清除,同时进行一次完整的可达性分析和活跃度分析,需要STW(由于1、2环节的工作,这一STW过程会很短)
  4. 拷贝/清除:这一阶段中,G1会选取活跃度较低的内存块,对其进行并行的垃圾回收,具体的垃圾回收算法和G1的Young GC一样,即将待清理的内存块中的存活对象拷贝至一或多个内存块。需要STW

值得注意的是,G1的垃圾回收并非每次都将所有不可达对象完全清除,G1倾向于优先清除活跃度低的内存块,因为这些块中的存活对象少(或者压根没有存活对象),清理速度更快。对于那些活跃度高的内存块,G1会放置不管,直至某个时间点该块的活跃度足够低时再进行回收。
正因为如此,G1进行垃圾回收的时机更早,默认在堆内存使用率达到45%时就会开始垃圾回收。这体现了G1(Garbage-First/垃圾回收优先)这一名字的含义。
至于活跃度多低才会进行回收,则是由G1决定的,G1会调整自己的回收策略来尽可能满足用户设置的最大STW时长。
同时,由于使用拷贝算法,G1回收器不会产生内存碎片,这也是相较CMS的巨大优势之一。
G1回收器能够根据需要动态调整各个分代的内存占比,在快速实例化大量对象时表现尤佳。
G1回收器接收一个最大STW时长的任务指标,并会努力控制每次垃圾回收的STW时长来完成这一指标。默认的指标是200ms,注意不要给G1设置过于过分的任务指标,否则它为了完成任务可能是会乱来的……

G1适合运行在配有强大CPU的主机上的,且占用较大堆内存空间的应用程序中。如果你想要得到最佳的STW时长,并且愿意为此进行一些参数调优工作的话,使用G1回收器通常来说是最好的选择。

通过参数-XX:+UseG1GC 来启用G1回收器,并使用-XX:MaxGCPauseMillis=n来设置最大的STW时长,-XX:InitiatingHeapOccupancyPercent=n来设置触发垃圾回收的堆内存占用比

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容