浅谈JAVA虚拟机中的GC

前言

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

JVM的分代思想

JVM根据对象存活周期不一样将heap划分红了新生代、老年代、永久代(方法区&元空间)。
有个问题,JVM是先有的分代思想而后根据不一样的代发展不一样的垃圾回收思想,仍是先有的垃圾回收思想才划分不一样的代? 多线程

JVM如何判断对象须要回收

JAVA与C有个很显著的不一样,就是JAVA不须要手动归还内存,彻底由GC自动管理内存回收。那么GC是如何判断对象是否须要回收的呢?并发

  • 引用计数法性能

    引用计数法是指在对象中添加一个引用计数器,若是被其余对象引用则计数器+1,引用失效时-1。
    优势:实现简单,判断效率也很高;
    缺点:存在对象循环引用问题,因此在主流的虚拟机中并无采用引用计数器。
    对象A持有对象B的引用,对象B持有对象A的引用,除此以外在无其余对象引用A和B,GC没法回收这样的对象.线程

  • 可达性分析3d

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

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

    总结

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

    • 第一次标记:当对象没法关联上GC Roots时会被第一次标记,并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()
      条件一:对象是否有重写finalize()方法
      条件二:虚拟机是否已经调用过该对象的finalize()方法
      筛选成功的对象会被放进一个队列,稍后会由JVM自动建立的线程去执行finalize()方法;
    • 第二次标记:第二次标记时若是对象在finalize()方法里关联上任何一条引用链,则会被移出即将要回收的集合,不然该对象真正“死亡”。

GC的垃圾收集算法

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

  • 标记-清除算法
    JVM分配内存时整个heap能够看作一个大的表格里有多个单元格,对于要回收的对象打上一个“标记”,而后对标记的对象进行“清除”,“标记-清除”也是最基础的思想,后面的几种思想都是基于这之上的改进。
    缺点:
    • 1.标记和清除过程效率都不高
    • 2.“标记-清除”后会产生大量不连续的内存碎片,当碰到须要分配较大对象内存时,没法找到连续的空间则会触发一次Young GC或者Full GC(两种GC的区别能够参考这篇文章 www.zhihu.com/question/41…)。

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

    后来IBM有专门研究新生代的对象大多朝夕生死(建立后很快会销毁),因此并不须要按1:1来分配,而是按8:1:1来划分,一块较大的Eden空间和两块较小的Survivor空间,每次分配占用Eden+一块Survivor空间(新对象的分配只会在Eden上),当垃圾回收时将存活的对象拷贝到另外一块Survivor,这样空间利用率达到90%。
    实际状况并非每次回收时一块Survivor都能装下全部存活对象,那这时就会经过“空间分配担保”的机制直接晋升到老年代。

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

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

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

JVM中的垃圾收集器

黄色表明只处理新生代的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模式的虚拟机。
  • ParNew
    ParNew收集器是Serial收集器的多线程版本,除使用多条线程进行垃圾收集以外,其他行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器彻底同样,在实现上,这两种收集器也共用了至关多的代码。

    • 缺点:存在STW,单核CPU性能比不会比Serial好。
    • 优势:Server模式下首选的新生代收集器,除了Serial外目前只有它能与CMS搭配使用,多核CPU状况下能有效利用系统资源。
  • Parallel Scavenge
    Parallel Scavenge收集器是一个并行的多线程年轻代收集器,其余收集器关心如何缩短垃圾收集的时间而它关注的是如何控制系统运行的吞吐量(吞吐量(吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间)))。高吞吐量能够高效率的利用CPU时间,尽快完成运算任务,只要适合在后台运算而不须要太多交互的任务。

    • 优势:能够精确控制吞吐量。
      • -XX:MaxGCPauseMillis用于控制最大垃圾收集停顿时间(一个大于0的整数,表明毫秒,收集器保证每次不超过这个时间,若是太小的话会频繁发生GC,反而会下降吞吐量)
      • -XX:GCTimeRatio用于直接控制吞吐量的大小(是一个0-100之间的整数,表示应用程序运行时间和垃圾收集时间的比值。默认值为99,即最大容许1%(1 / (1 + 99) = 1%)的垃圾收集时间)
      • -XX:UseAdaptiveSizePolicy虚拟机会根据当前系统的运行状况动态调整合适的设置值来达到合适的停顿时间和合适的吞吐量,这种方式称为GC自适应调节策略。
    • 缺点:参数设置不当的状况下可能会频繁发生GC。
  • Serial Old
    Serial的老年代版本,它也是一款使用"标记-整理"算法的单线程的垃圾收集器,优劣和Serial同样。有两大用途:

    • JDK1.5前与Parallel Scavenge搭配使用
    • 做为CMS发生Concurrent Mode Failure状况下老年代预备方案
  • 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)

      并发清除前面全部标记的对象。

    • 优势:
      • 并发收集:并发标记和并发清除两个耗时阶段是能够和用户线程并发执行的,而初始标记和从新标记耗时很短,因此基本上能够认为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运做过程大体类似,

相关文章
相关标签/搜索