浅谈JAVA虚拟机中的GC

前言

本文从JVM如何判定对象是否需要回收开始分析,再到JVM的几种垃圾回收思想如何产生,最后再来介绍JVM经典的7种垃圾回收器的特点(不包含ZGC);

JVM的分代思想

JVM根据对象存活周期不同将heap划分成了新生代、老年代、永久代(方法区&元空间)。

有个问题,JVM是先有的分代思想然后根据不同的代发展不同的垃圾回收思想,还是先有的垃圾回收思想才划分不同的代?

image

JVM如何判断对象需要回收

JAVA与C有个很显著的不同,就是JAVA不需要手动归还内存,完全由GC自动管理内存回收。那么GC是如何判断对象是否需要回收的呢?

  • 引用计数法

    引用计数法是指在对象中添加一个引用计数器,如果被其他对象引用则计数器+1,引用失效时-1。

    优点:实现简单,判断效率也很高;

    缺点:存在对象循环引用问题,所以在主流的虚拟机中并没有采用引用计数器。

    对象A持有对象B的引用,对象B持有对象A的引用,除此之外在无其他对象引用A和B,GC无法回收这样的对象.

  • 可达性分析

    在主流商用语言(JAVA/C#/Lisp)都是使用可达性分析算法来判定对象是否存活。主要思想就是通过一系列被称为GC Roots的对象作为起始点开始先下搜索,走过的路径称为引用链,如果某个对象没有任何一条到达GC Roots对象的引用链则代表此对象可回收的。

    JAVA中可以被称为GC Roots对象:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
    • 方法区中的类静态属性引用的对象;
    • 方法区中常量引用的对象;
    • 本地方法栈中JNI(即一般说的Native方法)中引用的对象;
    image

    总结

    GC Roots无法到达的对象并不是一定会被回收,一个对象至少要被标记两次才会真正死亡。

    • 第一次标记:当对象无法关联上GC Roots时会被第一次标记,并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()

      条件一:对象是否有重写finalize()方法

      条件二:虚拟机是否已经调用过该对象的finalize()方法

      筛选成功的对象会被放进一个队列,稍后会由JVM自动创建的线程去执行finalize()方法;
    • 第二次标记:第二次标记时如果对象在finalize()方法里关联上任何一条引用链,则会被移出即将要回收的集合,否则该对象真正“死亡”。

GC的垃圾收集算法

在JVM知道那些对象是可回收的后,需要开始真正的回收对象了。JVM在发展的过程中出现了几种经典的回收思想,这里不讨论每种算法具体如何实现(因为我也不了解...)。

  • 标记-清除算法

    JVM分配内存时整个heap可以看做一个大的表格里有多个单元格,对于要回收的对象打上一个“标记”,然后对标记的对象进行“清除”,“标记-清除”也是最基础的思想,后面的几种思想都是基于这之上的改进。

    缺点:
    • 1.标记和清除过程效率都不高
    • 2.“标记-清除”后会产生大量不连续的内存碎片,当碰到需要分配较大对象内存时,无法找到连续的空间则会触发一次Young GC或者Full GC(两种GC的区别可以参考这篇文章 https://www.zhihu.com/question/41922036)。
image
  • 复制算法

    为了解决效率问题,出现了一种复制的算法,一开始是将内存按1:1划分成两块,每次只在其中一块内存上分配对象,当触发垃圾回收时将存活的对象全部复制到另一块的内存上,然后把已经使用过的那快内存清空掉。这样既解决了效率问题也解决了内存碎片化的问题。但同时也带来了空间浪费的缺点:每次只能使用50%的空间

    image

    后来IBM有专门研究新生代的对象大多朝夕生死(创建后很快会销毁),所以并不需要按1:1来分配,而是按8:1:1来划分,一块较大的Eden空间和两块较小的Survivor空间,每次分配占用Eden+一块Survivor空间(新对象的分配只会在Eden上),当垃圾回收时将存活的对象拷贝到另一块Survivor,这样空间利用率达到90%。

    实际情况并不是每次回收时一块Survivor都能装下所有存活对象,那这时就会通过“空间分配担保”的机制直接晋升到老年代。
    image

  • 标记-整理算法

    由于老年代的对象都是长期存活,所以复制算法并不适用老年代,因此又提出了“标记-整理”算法,标记过程与“标记-清除”算法一样,只是后续并不是直接清除对象而是先将所有存活对象都向一端移动,然后直接清理掉边界以外的内存。

    image

  • 分代收集算法

    当前主流商用垃圾回收器都是采用的“分代收集算法”,这个算法并没有什么新的思想只是根据对象存活周期的不同将内存划分成不同的代然后采用不同的回收算法。

    • 新生代的对象常规来说每次只有少量对象存活,如果用“标记”思想的话则效率和规则的过程都会很慢,故而采用“复制算法”。
    • 老年代对象大多存活量高,又没有担保空间,就必须采用“标记-清除”or“标记-整理”。

JVM中的垃圾收集器

image

黄色代表只处理新生代的GC,蓝色代表只处理老年代GC,各GC之间的连线代表可以搭配使用。G1可以独立回收整个head;

在介绍这些收集器各自的特性之前,让我们先来明确一个观点:虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来,虽然垃圾收集器的技术在不断进步,但直到现在还没有最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,HotSpot虚拟机完全没必要实现那么多种不同的收集器了(摘选自《深入理解Java虚拟机(第2版)》)。

这里说明一下"并行"和"并发"的概念。

  • 并行(Parallel):多条垃圾收集线程并行工作,而此时用户线程仍处于等待状态。
  • 并发(Concurrent):垃圾收集线程与用户线程同时执行(不一定是并行有可能是交替执行),多核CPU的情况下不同的线程在不同的CPU上同时执行。
  • Serial

    Serial收集器是最基本历史最悠久的收集器,JDK1.3.1之前是新生代唯一的选择。Serial是一个单线程收集器,这里的“单线程”并不是指一个CPU或一条线程而是Serial在垃圾收集时必须暂停其他工作线程(Stop The World)也就是俗称的“STW”。

    • 缺点:单线程,存在STW。
    • 优点:简单高效,单核CPU没有线程交互的开销,适合Client模式的虚拟机。


      image
  • ParNew

    ParNew收集器是Serial收集器的多线程版本,除使用多条线程进行垃圾收集之外,其余行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

    • 缺点:存在STW,单核CPU性能比不会比Serial好。
    • 优点:Server模式下首选的新生代收集器,除了Serial外目前只有它能与CMS搭配使用,多核CPU情况下能有效利用系统资源。
      image
  • Parallel Scavenge

    Parallel Scavenge收集器是一个并行的多线程年轻代收集器,其他收集器关心如何缩短垃圾收集的时间而它关注的是如何控制系统运行的吞吐量(吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)))。高吞吐量可以高效率的利用CPU时间,尽快完成运算任务,只要适合在后台运算而不需要太多交互的任务。

    • 优点:可以精确控制吞吐量。
      • -XX:MaxGCPauseMillis用于控制最大垃圾收集停顿时间(一个大于0的整数,代表毫秒,收集器保证每次不超过这个时间,如果过小的话会频繁发生GC,反而会降低吞吐量)
      • -XX:GCTimeRatio用于直接控制吞吐量的大小(是一个0-100之间的整数,表示应用程序运行时间和垃圾收集时间的比值。默认值为99,即最大允许1%(1 / (1 + 99) = 1%)的垃圾收集时间)
      • -XX:UseAdaptiveSizePolicy虚拟机会根据当前系统的运行情况动态调整合适的设置值来达到合适的停顿时间和合适的吞吐量,这种方式称为GC自适应调节策略。
    • 缺点:参数设置不当的情况下可能会频繁发生GC。


      image
  • Serial Old

    Serial的老年代版本,它也是一款使用"标记-整理"算法的单线程的垃圾收集器,优劣和Serial一样。有两大用途:

    • JDK1.5前与Parallel Scavenge搭配使用
    • 作为CMS发生Concurrent Mode Failure情况下老年代预备方案
      image
  • Parllel Old

    Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"标记-整理"算法。JDK1.6才提供,在此之前Parallel Scavenge只能和单线程的Serial Old搭配使用,由于老年代的Serial Old在服务端拖累又不能有效利用多核CPU的处理能力,导致Parallel Scavenge的高吞吐名副其实。直到Parllel Old的出现“吞吐量优先”的收集器才有了用武之地,任何注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器一起配合使用

    • 优点:可以搭配Parallel Scavenge一起使用,多线程收集老年代
  • CMS

    真正意义上的一款具有划时代意义的垃圾收集器,基于“标记-清除”算法实现,关注点在获取最短停顿时间为目标,大量运用在B/S系统的服务端上。

    整个回收过程分为四个步骤:

    • 初始标记(CMS initial mark)

      标记GC Roots直接关联到的对象,速度很快。需要STW
      * 并发标记(CMS concurrent mark)
      > 标记GC Roots找到所有能关联到的对象
      * 重新标记(CMS remark)
      > 因为并发标记是和用户线程并发的所以在标记的过程中会产生新的对象,所以要重新标记。需要STW
      * 并发清除(CMS concurrent sweep)
      > 并发清除前面所有标记的对象。

    image
    • 优点:
      • 并发收集:并发标记和并发清除两个耗时阶段是可以和用户线程并发执行的,而初始标记和重新标记耗时很短,所以基本上可以认为CMS在垃圾收集时是和应用程序并发执行的。
      • 低停顿
    • 缺点:
      • 对CPU资源敏感,并发阶段会占用部分CPU资源,导致程序变慢,吞吐量降低。
      • 无法处理浮动垃圾(并发标记阶段产生的垃圾只能下次回收处理),所以垃圾回收时要预留足够的空间给用户线程使用。
      • 因为采用“标记-清除”的算法,会产生大量空间碎片,从而导致老年代可能有很大空间剩余但是却无法找到足够大的连续空间分配大的对象,不得不提前触发Full GC(Full GC之前也有可能触发一次Young GC已降低Full GC的压力)。
  • G1

    G1全称“Garbage First”垃圾收集器直至JDK7,Sun公司才认为G1达到足够成熟的商用程度,目标是在未来可以替换掉CMS。之前的GC都只负责整个新生代/老年代,而G1可以独立负责整个Heap,G1是将整个Heap划分成多个大小相等的Region,逻辑上仍保留分代的概念,但已不是物理分隔了,它们都是一部分不需要连续的Region集合。

    G1有以下特点:

    • 并行并发:能够充分利用多核多CPU来缩短STW时间,部分其他GC需要用户线程停顿的地方,G1可以通过并发方式让用户线程继续执行。
    • 分代收集:虽然物理上不在划分新生代/老年代,但是分代思想仍在G1中保留,比如G1不需要搭配其他收集器就可以独立管理整个Heap堆,但是G1会对存活周期不同的对象采用不同的方式去处理。
    • 空间整合:整体来看G1采用“标记-整理”算法实现,从局部来看两个Region之间是基于“复制”算法实现的。不管怎么说G1不会像CMS一样出现内存碎片问题。
    • 可预测的停顿:G1除了追求低停顿以外,还能建立可预测的停顿时间模型(指定一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不超过N毫秒)。G1对每一个Region的垃圾堆积的价值大小维护了一个优先列表,每次根据允许的收集时间,优先回收价值大的Region(这就是Garbage First名称的由来),保证了有限的时间内获取尽可能高的收集效率。

    G1收集器的大概步骤:

    • 初始标记
    • 并发标记
    • 最终标记
    • 筛选回收:对各个Region的回收价值和成本进行排序,然后根据用户期望的停顿时间来制定回收计划。

    前三个步骤与CMS运作过程大致相似,

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