JVM的垃圾回收机制

JVM垃圾回收算法概述

Java 语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源,例如内存资源的释放情况。自动垃圾收集虽然大大减轻了开发人员的工作量,但是也增加了软件系统的负担。
Java在进行垃圾回收之前需要考虑两个问题,即:

  1. 哪些内存需要回收?
  2. 如何回收?
    其中,第一个问题描述的是如何判断对象已经不需要了,死了,可以回收了。第二个问题则解决了采用哪种垃圾回收算法来回收。下面详细的介绍上述3个问题的解决方法。

    如何判断对象可以进行回收?

    判断对象”已死”通常有引用计数法可达性分析算法两种方法。

    引用计数法

    引用计数器的实现很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。
    可以看出,引用计数法很简单,但是存在一种“循环引用”的问题。例如:有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。但是在系统中却不存在任何第 3 个对象引用他们。也就是说,AB 是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏

    可达性分析算法

    在主流的商用程序语言(Java、 C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。 这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
    可作为GC Roots的对象有:
    • 虚拟机栈(栈中的本地变量表)中引用的对象。
    • 方法区中类静态属性引用的对象。
    • 方法区中常量引用的对象

      对方法区(永久代)的回收

      永久代的垃圾收集主要回收两部分内容:废弃常量无用的类。 对于常量回收,只要这个常量没有被任何引用,那么就可以进行垃圾回收。要判断一个类是否是无用的类的条件则要苛刻的多,满足以下条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

      垃圾回收算法

      标记-清除算法

      该算法分为两个阶段,即标记清除。通过可达性分析算法,标记的阶段就是找出可以回收的对象。清除就是将这些区域重新可以使用。
      该算法有两个不足:
    • 效率问题。标记和清除的效率都不高。
    • 空间问题。进行标记-清除算法后,将会产生不连续的内存碎片,可能导致无法分配大对象而又进行垃圾收集。
      算法的描述如图所示:
      标记清除算法图

      复制算法

      为了解决效率问题,该算法将内存区域划分为两个相同的部分,记为A和B。一次只使用其中的一半,在需要对A进行垃圾收集时,则将A中存活的对象复制到区域B,然后对A进行清空即可。
      该算法不会产生碎片,实现简单,运行高效。但是该算法使得可用的内存下降为一半,代价太高了。
      算法的描述如图所示:
      复制算法图
      现在的虚拟机通常采用该算法进行新生代的垃圾收集,然而并不是按照1:1的比例来进行内存划分,而是将内存划分为一块较大的Eden区和两个较小的Survivor区域(如A和B),每次进行内存分配只在Eden区和Survivor A区进行。当进行垃圾回收时,将上述两个区域中存活的对象全部复制到Survivor B区域,最后,对这两个区域进行清空。
      复制算法存在一种内存担保的现象,指的是当EdenSurvivor A区域存货的对象太多,Survivor B中放不下了,那么这些对象将直接进入老年代

      标记-整理算法

      复制算法在对象存活率比较高的情况下,效率降低,因此复制算法不适用于老年代的垃圾收集。
      根据老年代的特点,标记-整理算法在对存活的对象进行标记后,不是将对象进行清除,而是将对象有序的向一端移动,然后清理掉边界以外的内存。
      算法的描述图如下:
      标记整理图

      分代收集算法

      分代收集指的是根据Java中不同区域中的对象的生存时间不同,将对象分为新生代老年代。针对不同的区域,来使用不同的收集算法。例如:新生代每次存活的对象很少,那么就可以采用复制算法来对该区域进行垃圾回收;老年代每次收集时存活的对象较多,那么就可以采用标记-清除或者标记-整理算法来进行收集。

总结

本文介绍了Java中的垃圾收集机制以及常见的垃圾收集算法。Java的垃圾收集机制是Java的一个重要特性,用户无需手动释放内存,Java的GC机制能够满足大多数场景对于内存分配的要求。同时,本文在介绍了Java垃圾回收需要解决的问题,即如何判断对象已经可以回收以及如何进行回收(垃圾回收算法)。