【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法

一、什么是垃圾

在 JVM 进行垃圾回收之前,首先就是判断哪些对象是垃圾,也就是说,要判断哪些对象是可以被销毁的,其占有的空间是可以被回收的。根据 JVM 的架构划分,我们知道, 在 Java 世界中,几乎所有的对象实例都在堆中存放,所以垃圾回收也主要是针对堆来进行的。

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。

二、垃圾判断算法

  1. 引用计数法
    在这种算法中,假设堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并且初始化赋值后,该对象的计数器的值就设置为 1,每当有一个地方引用它时,计数器的值就加 1,例如将对象 b 赋值给对象 a,那么 b 被引用,则将 b 引用对象的计数器累加 1。
    反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。
    特别地,当一个对象被当做垃圾收集时,它引用的任何对象的计数器的值都减 1。
  • 优点 :引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
  • 缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。
  1. 可达性分析法
    可达性分析法也被称之为根搜索法,可达性是指,如果一个对象会被至少一个在程序中的变量通过直接或间接的方式被其他可达的对象引用,则称该对象就是可达的。更准确的说,一个对象只有满足下述两个条件之一,就会被判断为可达的:
  • 对象是属于根集中的对象
  • 对象被一个可达的对象引用

在这里,我们引出了一个专有名词,即根集,其是指正在执行的 Java 程序可以访问的引用变量(注意,不是对象)的集合,程序可以使用引用变量访问对象的属性和调用对象的方法。
根集中的对象称之为GC Roots,也就是根对象。可达性分析法的基本思路是:将一系列的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,如果一个对象到根对象没有任何引用链相连,那么这个对象就不是可达的,也称之为不可达对象。

三、垃圾回收算法

  1. 标记-清除算法
    标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。
  • 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
  • 缺点:标记和清除过程的效率都不高,这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小,对空闲列表的管理会增加分配对象时的工作量;标记清除后会产生大量不连续的内存碎片,虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败,不得不触发另一次垃圾收集动作。

下图为标记-清除算法回收前后的状态:
【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法_第1张图片

  1. 标记-整理算法

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

  • 优点:经过整理之后,新对象的分配只需要通过指针碰撞便能完成,比较简单;使用这种方法,空闲区域的位置是始终可知的,也不会再有碎片的问题了。
  • 缺点:GC 暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法_第2张图片
3. 复制算法

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。

复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如“标记-整理”算法。一种典型的基于复制算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。

  • 优点:标记阶段和复制阶段可以同时进行;每次只对一块内存进行回收,运行高效;只需移动栈顶指针,按顺序分配内存即可,实现简单;内存回收时不用考虑内存碎片的出现。
  • 缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
    下图为复制算法回收前后的状态:
    【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法_第3张图片
  1. 分代收集算法
    分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。
    【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法_第4张图片
  • 新生代(Young Generation):几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照 8:1:1 的比例分为一个 Eden 区和两个 Survivor(Survivor0,Survivor1)区。大部分对象在 Eden 区中生成。当新对象生成,Eden 空间申请失败(因为空间不足等),则会发起一次 GC(Scavenge GC)。回收时先将 Eden 区存活对象复制到一个 Survivor0 区,然后清空 Eden 区,当这个 Survivor0 区也存放满了时,则将 Eden 区和 Survivor0 区存活对象复制到另一个 Survivor1 区,然后清空 Eden 和这个 Survivor0 区,此时 Survivor0 区是空的,然后将 Survivor0 区和 Survivor1 区交换,即保持 Survivor1 区为空, 如此往复。当 Survivor1 区不足以存放 Eden 和 Survivor0 的存活对象时,就将存活对象直接存放到老年代。当对象在 Survivor 区躲过一次 GC 的话,其对象年龄便会加 1,默认情况下,如果对象年龄达到 15 岁,就会移动到老年代中。若是老年代也满了就会触发一次 Full GC,也就是新生代、老年代都进行回收。
  • 老年代(Old Generation):在新生代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组。当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和 JVM 的相关参数。
  • 永久代(Permanent Generation):用于存放静态文件(class类、方法)和常量等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。对永久代的回收主要回收两部分内容:废弃常量和无用的类。永久代在 Java SE8 特性中已经被移除了,取而代之的是元空间(MetaSpace)

特别地,在分代收集算法中,对象的存储具有以下特点:
1.对象优先在 Eden 区分配。
2.大对象直接进入老年代。
3.长期存活的对象将进入老年代,默认为 15 岁。

除此之外,我们再来简单了解一下 GC 的分类:

  • 新生代 GC(Minor GC / Scavenge GC):发生在新生代的垃圾收集动作。因为 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 非常频繁(不一定等 Eden 区满了才触发),一般回收速度也比较快。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。
  • 老年代 GC(Major GC / Full GC):发生在老年代的垃圾回收动作。Major GC 经常会伴随至少一次 Minor GC。由于老年代中的对象生命周期比较长,因此 Major GC 并不频繁,一般都是等待老年代满了后才进行 Full GC,而且其速度一般会比 Minor GC 慢10倍以上。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”算法或“标记-整理”算法来进行回收。

新生代采用空闲指针的方式来控制 GC 触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发 GC。当连续分配对象时,对象会逐渐从 Eden 到 Survivor,最后到老年代。

四、垃圾回收器

垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令 GC 线程工作。以串行模式工作的收集器,称为串行收集器;与之相对的是以并行模式工作的收集器,称为并行收集器。

  1. Serial 收集器
    串行收集器采用单线程方式进行收集,且在 GC 线程工作时,系统不允许应用线程打扰。此时,应用程序进入暂停状态,即 Stop-the-world。Stop-the-world 暂停时间的长短,是衡量一款收集器性能高低的重要指标。Serial 是针对新生代的垃圾回收器,采用“复制”算法。

  2. ParNew 收集器
    并行收集器充分利用了多处理器的优势,采用多个 GC 线程并行收集。可想而知,多条 GC 线程执行显然比只使用一条 GC 线程执行的效率更高。一般来说,与串行收集器相比,在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。ParNew 是针对新生代的垃圾回收器,采用“复制”算法,可以看成是 Serial 的多线程版本

  3. Parallel Scavenge 收集器
    Parallel Scavenge 是针对新生代的垃圾回收器,采用“复制”算法,和 ParNew 类似,但更注重吞吐率。在 ParNew 的基础上演化而来的 Parallel Scanvenge 收集器被誉为“吞吐量优先”收集器。吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如虚拟机总运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是99%。

  4. Serial Old 收集器
    Serial Old 是 Serial 收集器的老年代版本,单线程收集器,采用“标记-整理”算法。这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。

  5. Parallel Old 收集器
    Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多线程收集器,采用“标记-整理”算法。

  6. CMS收集器
    CMS(Concurrent Mark Swee)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集,采用“标记-清除”算法,它的运作过程分为 4 个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

CMS 收集器优点:并发收集,低停顿。
CMS 收集器缺点:
CMS 收集器对 CPU 资源非常敏感;
CMS 收集器无法处理浮动垃圾;
CMS 收集器是基于“标记-清除”算法,标记和清除过程的效率都不高。

CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面已经介绍过“标记-清除”算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供CMS 版本。

  1. G1 收集器

G1(Garbage First)重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成,即 G1 提供了接近实时的收集特性。G1 与 CMS 的特征对比如下:
【JVM】深入理解JVM垃圾回收机制及其垃圾回收算法_第5张图片
G1 具备如下特点:

  • 并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-the-world 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
  • 分代收集:打破了原有的分代模型,将堆划分为一个个区域。
  • 空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
  • 可预测的停顿:这是 G1 相对于 CMS 的一个优势,降低停顿时间是 G1 和 CMS 共同的关注点。

博客参考:
深入理解 JVM 垃圾回收机制及其实现原理

你可能感兴趣的:(JVM,操作系统,jvm,算法,java)