Java垃圾回收器及其算法详解

1 垃圾回收器的位置

在这里插入图片描述

2 垃圾回收器的基本概念

  什么是垃圾回收器:JVM 为 Java 提供了垃圾回收机制,是一种偏自动的内存管理机制。简单来说,垃圾回收器会自动追踪所有正在使用的对象,并将其余未被使用的对象标记为垃圾,JVM会自动进行垃圾回收,释放内存。

  哪些内存需要回收:不可能再被任何途径所使用的对象,Java中通过可达性分析法来检测对象是否为垃圾,如果不可达,则将对象标记为垃圾,等待 JVM 回收。

3 可达性分析法

3.1原理

  通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当GC Roots到对象不可达时证明此对象是不可用的。
  那么如何选取 GCRoots 对象呢?在 Java 语言中,可以作为 GCRoots 的对象包括下面几种:
在这里插入图片描述

  虚拟机栈(栈帧的局部变量表)中的引用的对象:我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的;
  全局的静态的对象:也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为 GC Roots 是必须的;
  常量引用:就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为 GC Roots;
  Native 方法引用对象:这一种是在使用 JNI 技术时,有时候单纯的 Java 代码并不能满足我们的需求,我们可能需要在 Java 中调用 C 或 C++ 的代码,因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为 GC Roots。
  上图中,对象 A,B,C,D,E,F 为可达对象;而对象 G,H,I,J,K 为不可达对象,会被标记为垃圾对象,最终被垃圾回收器回收。

3.2 可达性分析的四种引用类型

  由上可知,可达性分析的 GC Roots 均为引用对象,而引用对象有 4 种引用类型:
在这里插入图片描述

3.2.1 强引用

  定义:强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  代码示例:

public class DemoTest { 
    public static void main(String[] args) { 
        Object obj = new Object(); // 强引用
    }
}

  在强引用的定义中有这样一句话:“只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。” 那么有没有办法将强引用消除呢?
  消除强引用示例代码:

public class DemoTest { 
    public static void main(String[] args) { 
        Object obj = new Object(); // 强引用
        obj = null; //消除强引用
    }
}

  如果不使用强引用时,可以赋值 obj=null,显示的设置 obj 为 null,则 gc 认为该对象不存在引用,这时候就可以回收此对象。

3.2.2 软引用

  软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,如果内存充足,则垃圾回收器不会回收该对象,如果内存不够了,就会回收这些对象的内存。
  在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  软引用使用场景
  软引用主要应用于内存敏感的高速缓存,在 Android 系统中经常使用到。一般情况下,Android 应用会用到大量的默认图片,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。
  但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生 OutOfMemory 异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。
  SoftReference 可以解决 OOM 的问题,每一个对象通过软引用进行实例化,这个对象就以cache的形式保存起来,当再次调用这个对象时,那么直接通过软引用中的 get() 方法,就可以得到对象中的资源数据,这样就没必要再次进行读取了,直接从 cache 中就可以读取得到,当内存将要发生 OOM 的时候,GC 会迅速把所有的软引用清除,防止 OOM 发生。

3.2.3 弱引用

  定义:弱引用描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。
  代码示例:

import java.lang.ref.WeakReference;
 
public class Main { 
    public static void main(String[] args) {     
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));         
        System.out.println(sr.get());
        System.gc();                //通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

  结果验证:第二个输出结果是 null,这说明只要 JVM 进行垃圾回收,被弱引用关联的对象必定会被回收掉。

hello
null

3.2.4 虚引用

  顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用在 Java 中使用 java.lang.ref.PhantomReference 类表示。
  作用:虚引用主要用来跟踪对象被垃圾回收的活动。
  虚引用与软引用和弱引用的区别:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
  使用示例:虚引用必须和引用队列(ReferenceQueue)联合使用

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main { 
    public static void main(String[] args) { 
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

4 常见的垃圾回收算法

4.1 垃圾回收算法种类

  垃圾回收算法有几种?如果单纯从一些博客或者论坛上的内容来说,部分作者会将垃圾回收分为如下 4 种算法:

  • 标记-清除(Mark-Sweep)算法;
  • 复制(coping)算法;
  • 标记-整理(Mark-Compact)算法;
  • 分代收集算法。

  但是这种分类是不准确的,准确来说,垃圾回收只有 3 种算法:

  • 标记-清除(Mark-Sweep)算法;
  • 复制(coping)算法;
  • 标记-整理(Mark-Compact)算法。

  为什么会有所谓的“分代收集算法”呢? 此处埋下一个伏笔。

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

  标记 - 清除(Mark-Sweep)算法是最基本的算法,就如同它的名字一样,分为“标记”和“清除”两个阶段:
  a.首先标记出所有需要回收的对象,这就是标记阶段;
  b.标记完成后统一回收所有被标记的对象,这就是所谓的清除阶段。

  缺点:这种算法的不足主要体现在效率和空间。
  从效率的角度讲:标记和清除两个过程的的效率都不高
  从空间的角度讲:标记清除后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作。
  为了更加透彻的理解标记-清除(Mark-Sweep)算法,我们来看如下示意图。
在这里插入图片描述

4.3 复制(coping)算法

  Tips:前文提到过,标记-清除(Mark-Sweep)算法从效率的角度讲,"标记"和"清除"两个过程的的效率都不高,为了提升效率,提出了复制(coping)算法。
  基本概念:复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中的一块,当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配的执行过程如下图所示:
在这里插入图片描述

  缺点:使用这种算法后,内存缩小为原来的一半,代价太高了。

4.4 标记-整理(Mark-Compact)算法

  Tips:复制算法在对象存活率较高的场景下要进行大量的复制操作,效率还是很低。并且每次只使用一半的内存空间,资源浪费严重。标记-整理(Mark-Compact)算法解决了内存利用率的问题,并且减少了大量复制的问题。

  根据老年代的特点,有人提出了标记-整理(Mark-Compact)算法,标记过程与标记-整理(Mark-Compact)算法一样,不过不是直接对可回收对象进行整理,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存。标记-整理算法的工作过程如图:
在这里插入图片描述

4.5 分代清理

  问题:我们上文埋下了伏笔,分代清理到底是不是第四种算法呢?
  解答:不是,我们通常称之为分代收集理论,或称之为分代收集思想。目前虚拟机基本都采用分代收集理论来进行垃圾回收。
  分代收集理论结合了以上的 3 种算法,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。准确的说,分代收集理论就是在不同的内存区域使用不同的算法,它是 以上 3 种算法的使用者。
  因此说,分代清理并非是一种单独的算法,而是一种收集理论。
在这里插入图片描述

5 JVM 垃圾回收器分类

  本节主要讲解 7 种垃圾回收器,其中有 3 种垃圾回收器是作用于年轻代垃圾回收的收集器;另外 3 种圾回收器是作用于老年代垃圾回收的收集器;剩余的 1 种垃圾回收器能够同时作用于年轻代和老年代。
在这里插入图片描述

5.1 Serial收集器

  基本概念:Serial收集器是最基本、发展历史最久的收集器,这个收集器是采用复制算法的单线程的收集器。
  Tips:从概念上来看,我们需要注意Serial收集器的两个特点:一个是采用复制算法,另外一个是单线程收集
  单线程的收集器:单线程一方面意味着他只会使用一个 CPU 或者一条线程去完成垃圾收集工作,另一方面也意味着他进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。
  不过实际上到目前为止,Serial收集器依然是虚拟机运行在 Client 模式下的默认新生代收集器,因为它简单而高效。Serial 收集器运行过程如下图所示:
在这里插入图片描述

5.2 Parnew收集器

  基本概念:Parnew 收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样,但是他却是 Server 模式下的虚拟机首选的新生代收集器。
  Tips:从概念上来看,我们需要注意Parnew收集器的两个特点:一个是采用复制算法,另外一个是多线程收集
  特点
  除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。CMS 收集器第一次实现了让垃圾收集器与用户线程基本上同时工作;
  Parnew 收集器默认开启的收集线程数与 CPU 数量相同,在 CPU 数量非常多的情况下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
  Parnew 收集器运行过程如图所示:
在这里插入图片描述

5.3 Parallel Scavenge收集器

  基本概念:Parallel Scavenge 收集器也是一个新生代收集器,也采用了复制算法,也是并行的多线程收集器。Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。Parallel Scavenge 收集器是虚拟机运行在 Server 模式下的默认垃圾收集器。被称为“吞吐量优先收集器”。
  Tips:从概念上来看,我们需要注意Parallel Scavenge收集器的三个个特点:一个是采用复制算法,一个是多线程收集,一个是达到控制吞吐量的目标。
  Parallel Scavenge 收集器运行过程同 Parnew 收集器一样:
在这里插入图片描述

  控制吞吐量:CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
  吞吐量参数介绍:虚拟机提供了-XX:MaxGCPauseMills 和 -XX:GCTimeRatio 两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC 停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge 收集器也被称为“吞吐量优先收集器”。
  Parallel Scavenge 收集器有一个参数 -XX:UseAdaptiveSizePolicy 参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了,虚拟机会根据当前系统的运行情况以及性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
  如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,可以使用 Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。

5.4 Serial Old收集器

  基本概念: Serial Old 收集器同样是一个单线程收集器,作用于老年代,使用“标记-整理算法”,这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
  Serial Old 收集器运行过程如图所示:
在这里插入图片描述

5.5 Parallel Old收集器

  基本概念: Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理算法”进行垃圾回收。
  这个收集器在 JDK 1.6 之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge收集器+Parallel Old收集器 的组合。
  Parallel Scavenge 收集器+Parallel Old 收集器 的组合运行过程如下图所示:
在这里插入图片描述

5.6 CMS收集器

  基本概念:CMS(Conrrurent Mark Sweep,连续标记扫描)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法。
  收集步骤

  • 初始标记:标记 GCRoots 能直接关联到的对象,时间很短;
  • 并发标记:进行 GCRoots Tracing(可达性分析)过程,时间很长;
  • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
  • 并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。

  CMS 收集器运行过程如下图所示:
在这里插入图片描述

5.7 G1收集器

  基本概念:G1 是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器。
  特点
  a.并发和并行:使用多个 CPU 来缩短 Stop The World 停顿时间,与用户线程并发执行;
  b.分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果;
  c.空间整合:基于标记-整理算法,无内存碎片产生;
  d.可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
  在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region 的集合。

ps:以上内容来自对慕课教程的学习与总结。

正文到此结束
评论插件初始化中...
Loading...