垃圾标记阶段:对象存活标准

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以成为垃圾标记阶段

引用计数法

引用计数算法(Reference Counting) 比较简单,对每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。

  • 优点:实现简单,垃圾对象便于辨识:判定效率高,回收没有延迟性
  • 缺点:
    • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
    • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
    • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致Java的垃圾回收器中没有使用这类算法。

可达性标记算法

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄露的发生。

相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集(Tracing Garbage Collection)。

基本思路

  1. 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference chain)
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
  4. 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活对象。

GC Roots

  • 虚拟机栈中引用的对象
    • 比如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈内JNI(通常说的本地方法)引用的对象
  • 方法区中类静态属性引用的对象
    • 比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象
    • 比如:字符串常量池(String Table)里的引用
  • 所有被同步锁synchronized持有的对象
  • 虚拟机内部的引用
    • 基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
  • 反映java虚拟机内部情况的JMXBean、JVMMIT中注册的回调、本地代码缓存等。

注意

  • 如果要使用可达性分析算法判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
  • 这点也是导致GC进行时必须“Stop The World”的一个重要原因。
    • 即使是号称(几乎)不会发生挺短的CMS收集器中,枚举根节点时也是必须要停顿的。

对象的Finalization机制

  • Java语言提供对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
  • 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会调用这个对象的finalize()方法。
  • finalize()方法允许子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。
  • 永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。
    • 在finalize()时可能会导致对象复活。
    • finalize()方式执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
    • 一个糟糕的finalize()会严重影响GC的性能

垃圾清除阶段

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

背景

标记-清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应用于Lisp语言。

执行过程

当堆中的有效空间(Avaliable memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项是标记,第二项则是清除。

  • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • 清除:Collector对堆内存从头到尾线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

    缺点

  • 效率不算高

  • 在进行GC的时候,需要停止整个应用程序,导致用户体验差
  • 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。

复制(copying)算法

背景

为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文,“使用双存储区的Lisp语言垃圾收集器CALISP Garbage Collector Algorithm Using Serial Secondary Storage”。M.L.Minsky在论文中描述的算法被人们成为复制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

核心思想

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点

  • 没有标记和清除过程,实现简单,运行高效
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题

    缺点

  • 此算法的缺点也是很明显,就是需要两倍的内存空间。

  • 对于G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
  • 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会很大,或者非常低才行。

标记-压缩(Mark-Compact)算法

执行过程

  • 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
  • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
  • 之后,清理边界外所有的空间

标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,可以把它称为标记-清除-整理(Mark-Sweep-Compact)算法

二者本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。

优点

  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • 消除了复制算法,内存减半的高额代价

    缺点

  • 从效率上来说,标记-整理算法要低于复制算法

  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • 移动过程中,需要全程暂停用户应用程序。