转载

浅谈JAVA虚拟机中的GC

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

JVM的分代思想

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

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

浅谈JAVA虚拟机中的GC

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方法)中引用的对象;
    浅谈JAVA虚拟机中的GC

    总结

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

    • 第一次标记:当对象无法关联上 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… )。
浅谈JAVA虚拟机中的GC
  • 复制算法

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

    浅谈JAVA虚拟机中的GC
    后来IBM有专门研究新生代的对象大多朝夕生死(创建后很快会销毁),所以并不需要按1:1来分配,而是按8:1:1来划分,一块较大的Eden空间和两块较小的Survivor空间, 每次分配占用Eden+一块Survivor空间(新对象的分配只会在Eden上),当垃圾回收时将存活的对象拷贝到另一块Survivor ,这样空间利用率达到90%。
    实际情况并不是每次回收时一块Survivor都能装下所有存活对象,那这时就会通过“空间分配担保”的机制直接晋升到老年代。
    浅谈JAVA虚拟机中的GC
  • 标记-整理算法

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

    浅谈JAVA虚拟机中的GC
  • 分代收集算法

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

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

JVM中的垃圾收集器

浅谈JAVA虚拟机中的GC
黄色代表只处理新生代的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模式的虚拟机。
      浅谈JAVA虚拟机中的GC
  • ParNew

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

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

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

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

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

    • JDK1.5前与Parallel Scavenge搭配使用
    • 作为CMS发生 Concurrent Mode Failure 情况下老年代预备方案
      浅谈JAVA虚拟机中的GC
  • 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)

      并发清除前面所有标记的对象。

    浅谈JAVA虚拟机中的GC
    • 优点:
      • 并发收集:并发标记和并发清除两个耗时阶段是可以和用户线程并发执行的,而初始标记和重新标记耗时很短,所以基本上可以认为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运作过程大致相似,

    浅谈JAVA虚拟机中的GC
原文  https://juejin.im/post/5efc45f25188252e4839a275
正文到此结束
Loading...