0%

JVM的垃圾回收机制

  • JVM的垃圾回收
    • 垃圾回收算法
    • 垃圾回收器

1.6 ★JVM的垃圾回收

  • Java 中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection,简称 GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收

1.6.1 方法区回收

  • 方法区中能回收的内容主要就是不再使用的类。

    判定一个类可以被卸载。需要同时满足下面三个条件:

    1. 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
    2. 加载该类的类加载器已经被回收。
    3. 该类对应的 java.lang.Class 对象没有在任何地方被引用。

1.6.2 堆回收

1.6.2.1 引用计数法和可达性分析法
  • 如何判断堆上的对象可以回收?

    • Java 中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
    • 常见的判断方法有两种:引用计数法和可达性分析法。
  • 引用计数法:为每个对象维护一个引用计数器,当对象被引用时加 1,取消引用时减 1。

    • 引用计数法的优点是实现简单,但是它也存在缺点,主要有两点:
      1. 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响。
      2. 存在循环引用问题,所谓循环引用就是当 A 引用 B,B 同时引用 A 时会出现对象无法回收的问题。
  • 可达性分析法Java 使用的是可达性分析算法来判断对象是否可以被回收,可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。

    • 下图中 A 到 B 再到 C 和 D,形成了一个引用链,可达性分析算法指的是如果从某个对象到 GC Root 对象是可达的,那这个对象就不可被回收

    • 哪些对象被称之为 GC Root 对象呢?

      • 线程 Thread 对象。
      • 系统类加载器加载的 java.lang.Class 对象。
      • 监视器对象,用来保存同步锁 synchronized 关键字持有的对象。
      • 本地方法调用时使用的全局对象。
1.6.2.2 五种对象引用
强引用
  • 可达性分析算法中描述的对象引用,一般指的是强引用,即是 GC Root 对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java 中还设计了几种其他引用方式。
软引用
  • 软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。软引用常用于缓存中。
    • 软引用的执行过程如下:
      1. 将对象使用软引用包装起来,new SoftReference<对象类型>(对象)
      2. 内存不足时,虚拟机尝试进行垃圾回收。
      3. 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
      4. 如果依然内存不足,抛出 OutOfMemory 异常。
  • 软引用中的对象如果在内存不足时回收,SoftReference 对象本身也需要被回收(盒子里的东西已经被回收了,盒子也没有存在的必要了)。如何知道哪些 SoftReference 对象需要回收呢?
    • SoftReference 提供了一套队列机制:
      1. 软引用创建时,通过构造器传入引用队列。
      2. 在软引用中包含的对象被回收时,该 SoftReference 对象会被放入引用队列。
      3. 通过代码遍历引用队列,将相关联的 SoftReference 的强引用删除。
弱引用
  • 弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收。在JDK 1.2 版之后提供了 WeakReference 类来实现弱引用,弱引用主要在 ThreadLocal 中使用。弱引用对象本身也可以使用引用队列进行回收。
虚引用和终结器引用
  • 这两种引用在常规开发中是不会使用的。
1.6.2.3 垃圾回收算法
  • Java 是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:
    1. 找到内存中存活的对象。
    2. 释放不再存活对象的内存,使得程序能再次利用这部分空间。
垃圾回收算法的评价标准
  • Java 垃圾回收过程会通过单独的 GC 线程来完成,但是不管使用哪一种 GC 算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为 Stop The World 简称 STW,如果 STW 时间过长则会影响用户的使用。
  • 判断 GC 算法是否优秀,可以从三个方面来考虑:
    1. 吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC 时间)
      • 吞吐量数值越高,垃圾回收的效率就越高。
    2. 最大暂停时间:所有垃圾回收过程中 STW 时间最大值。
    3. 堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
标记-清除算法
  • 标记-清除算法的核心思想分为两个阶段:

    1. 标记阶段:将所有存活的对象进行标记,Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
    2. 清除阶段:从内存中删除没有被标记也就是非存活对象。
    • 优点:
      1. 实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
    • 缺点:
      1. 两次遍历:标记-清楚算法需要两次遍历堆内存。
      2. 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小而无法进行分配。
      3. 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法
  • 复制算法的核心思想是:

    1. 准备两块空间 From 空间和 To 空间,每次在对象分配阶段,只能使用其中一块空间(From 空间)。
    2. 在垃圾回收 GC 阶段,将 From 中的存活对象复制到 To 空间。
    3. 将两块空间的 FromTo 名字互换(保证存活对象一定是在 From 区中)。
    4. 将互换名字后的 To 空间中的对象清除。
    • 优点:
      1. 吞吐量高:复制算法只需要遍历一次存活对象复制到 To 空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动。
      2. 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入 To 空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
    • 缺点:
      1. 内存使用效率低:每次只能让一半的内存空间来为创建对象使用。
      2. 对象移动:在复制算法中需要进行对象的移动,会降低效率。
标记-整理算法
  • 标记-整理算法也叫标记压缩算法,是对标记-清理算法中容易产生内存碎片问题的一种解决方案。核心思想分为两个阶段:

    1. 标记阶段:将所有存活的对象进行标记,Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
    2. 整理阶段:将存活对象移动到堆的一端,清理掉非存活对象的内存空间。
    • 优点:
      1. 内存使用率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存。
      2. 不会发生内存碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间。
    • 缺点:
      1. 效率相对不高:既有标记-清除算法的两次遍历,又有复制算法的对象移动。
★分代GC
  • 现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。

    • 分代垃圾回收将整个内存区域划分为新生代和老年代。
      • 新生代:存放存活时间比较短的对象。
        • 又分为伊甸园区 Eden 和两个幸存者区 SurvivorFromTo)。
      • 老年代:存放存活时间比较长的对象。
  • 分代 GC 算法的核心思想:

    1. 新创建的对象,都会先分配到 Eden 区。
    2. 当伊甸园区内存不足,标记伊甸园与 from 区的存活对象。
    3. 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放,from 区和 to 区互换名字。
    4. 经过一段时间后伊甸园的内存又出现不足,标记 Eden 区域和 from 区存活的对象,将其复制到 to 区,复制完毕后,from 区和 to 区互换名字。
    5. 当幸存区对象熬过几次回收(最多 15 次)后,就晋升到老年代(幸存区内存不足或大对象会提前晋升)。
    6. 当老年代中空间不足,无法放入新的对象时,先尝试 minor gc,如果还是不足,就会触发 Full GCFull GC 会对整个堆进行垃圾回收。
    7. 如果 Full GC 依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出 Out Of Memory 异常。
1.6.2.4 垃圾回收器
  • 垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为新生代和老年代,除了 G1 之外其他垃圾回收器必须成对组合进行使用。具体关系图如下:
Serial-Serial Old
  • Serial 是一种单线程串行回收年轻代的垃圾回收器。
    • Serial 使用复制算法。
  • SerialOldSerial 垃圾回收器的老年代版本,采用单线程串行回收。
    • SerialOld 使用标记整理算法。
Parallel Scavenge-Parallel Old
  • Parallel Scavenge 是 JDK8 默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。
    • Parallel Scavenge 使用复制算法。
  • Parallel Old 是为 Parallel Scavenge 收集器设计的老年代版本,利用多线程并发收集。
    • Parallel Old 使用标记整理算法。
ParNew-CMS
  • ParNew 垃圾回收器本质上是对 Serial 在多 CPU 下的优化,使用多线程进行垃圾回收。

    • ParNew 使用复制算法。
  • CMS(Concurrent Mark Sweep)垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。

    • CMS 使用标记清除算法。

    CMS 的执行步骤:

    1. 初始标记,用极短的时间标记出 GC Roots 能直接关联到的对象。
    2. 并发标记,标记所有的对象,用户线程不需要暂停。
    3. 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
    4. 并发清理,清理死亡的对象,用户线程不需要暂停。
    • 缺点:
      1. CMS 使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS 会在 Full GC 时进行碎片的整理,这样会导致用户线程暂停。
      2. 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。
      3. 如果老年代内存不足无法分配对象,CMS 就会退化成 Serial Old 单线程回收老年代。
★G1
  • JDK9 之后默认的垃圾回收器是 G1(Garbage First) 垃圾回收器。

    • Parallel Scavenge 关注吞吐量,允许用户设置最大暂停时间,但是会减少新生代可用空间的大小。
    • CMS 关注暂停时间,但是吞吐量方面会下降。

    而 G1 设计目标就是将上述两种垃圾回收器的优点融合:

    1. 支持巨大的堆空间回收,并有较高的吞吐量。
    2. 支持多 CPU 并行垃圾回收。
    3. 允许用户设置最大暂停时间。
  • G1 出现之前的垃圾回收器,内存结构一般是连续的:

    G1 的整个堆会被划分成多个大小相等的区域,称之为区 Region,区域不要求是连续的。分为 EdenSurvivorOld 区。

    • Region 的大小通过堆空间大小 / 2048计算得到。
    • 也可以通过参数 -XX:G1HeapRegionSize=32m 指定(其中 32m 指定 region 大小为32M),Region size 必须是 2 的指数幂,取值范围从 1M 到 32M。
  • G1 垃圾回收有两种方式:

    1. 新生代回收(Young GC

      • 回收 Eden 区和 Survivor 区中不用的对象。采用复制算法来完成。
    2. 混合回收(Mixed GC

      • 回收所有新生代和部分老年代的对象以及大对象区。采用复制算法来完成。

        G1 对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是 G1(Garbage first) 名称的由来。

    • 注意:如果清理过程中发现没有足够的空 Region 存放转移的对象,会出现 Full GC,会单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。
---------------The End---------------