天天看点

垃圾回收之标记算法

扫描下方二维码或者微信搜索公众号

菜鸟飞呀飞

,即可关注微信公众号,阅读更多

Spring源码分析

Java并发编程

Netty源码系列

MySQL工作原理

文章。
垃圾回收之标记算法

前言

作为 Java 开发人员,其实是非常幸福的,因为 JVM 的存在,使得 Java 开发人员不需要像 C 或者 C++开发人员那样需要手动申请内存、释放内存,这些资源申请、垃圾回收的操作,JVM 底层直接帮助我们全干了。

这为 Java 开发人员省去了不少事情,但同样也使得像笔者这样的菜鸟,对垃圾回收的概念越来越模糊,甚至压根就不懂什么是垃圾回收。然而现在的面试官越来越坏,逮着程序员的薄弱环节使劲怼,特别喜欢问 JVM 相关知识,尤其是 JVM 调优经验、垃圾回收相关的知识。而作为一名有理想的菜鸟,最近埋头苦学了部分 JVM 知识,现在分享一波垃圾回收相关的知识。

垃圾回收

在 JVM 中,虚拟机规范将一大块内存细分为了很多不同的小区域,而 JVM 要想进行垃圾回收,首先得知道垃圾回收要回收的是哪些区域中的对象。下面这张图相信大家已经见过很多次了,它是虚拟机规范中一张经典的 JVM 内存结构图。图中的运行时数据区包含 5 个部分:堆区、方法区、程序计数器、虚拟机栈、本地方法栈。其中程序计数器、虚拟机栈、本地方法栈是每个线程私有的区域,它们随着线程的创建而生,随着线程的死亡而消失,因此这部分区域不需要 JVM 单独对它们进行垃圾回收。而堆区和方法区中存放的是对象、常量池、类信息等数据,这些数据是所有线程共享的,它们的生命周期不会伴随着线程的生而生,死而死,它们需要 JVM 单独进行垃圾回收。

垃圾回收之标记算法

JVM内存结构

知道了 JVM 中垃圾回收的目标区域,但是要对这些区域中的垃圾进行回收,JVM 首先得知道哪些对象是垃圾。而判断一个对象是否是垃圾,通常有两种算法:引用计数算法、可达性分析算法,下面将依次介绍这两种算法。

引用计数算法

采用引用计数算法来判断一个对象是否存活,其原理是:为每一个对象分配一个计数器,当这个对象被另一个对象引用时,这个计数器就加一;当被另一个对象取消引用时,计数器就减一。当这个计数器的值为零时,就表示当前对象没有被任何对象所引用,那么这个对象就可以被垃圾回收器进行回收了。

引用技术算法实现起来十分简单,也十分高效。但是它有个致命的缺点,就是无法解决循环引用的问题。例如如下示例代码:

public class ReferenceCountTest {

    private ReferenceCountTest reference;

    public static void main(String[] args) {
 ReferenceCountTest objA = new ReferenceCountTest();  ReferenceCountTest objB = new ReferenceCountTest();  objA.reference = objB;  objB.reference = objA;   objA = null;  objB = null;  } } 
           

示例代码中,变量 objA 和 objB 相互之间循环引用,如果采用引用计数算法来判断对象是否存活的话,即使我们将 objA 和 objB 设置为 null 后,由于它们各自的引用计数器均为 1,垃圾回收器会认为 objA 和 objB 还有人在使用,因此不会回收 objA 和 objB。

正是因为引用计数算法无法解决循环引用的问题,因此目前 Java 中的垃圾回收器均没有使用引用计数算法来判断一个对象是否存活,而是采用下面即将介绍的可达性分析算法。

可达性分析算法

可达性分析算法的实现思路是:将一系列被称之为“GC Roots”的根对象作为起始节点,从这个根节点出发,通过引用关系向下寻找它可以到达的对象,寻找过程中经过的路线称之为引用链,一个系统中可以有多个根节点,也就是说 GC Roots 是一个节点的集合。如果一个对象无法通过任何一个 GC Roots 根节点找到,即 0 条引用链,那么这个对象就不是存活对象了,后面在进行垃圾回收时,可以被垃圾收集器回收。

垃圾回收之标记算法

可达性分析算法

如果要使用可达性分析算法来进行垃圾标记,那么就必须保证在整个可达性分析过程当中,系统必须处于一致性快照当中。什么意思呢?就是在可达性分析过程中,不能有用户线程更新对象间的引用关系,否则可达性分析算法的分析结果的准确性就无法保证了。 因此在可达性分析算法的工作当中,会暂停所有的用户线程,也就是”Stop The World“,简称 STW。

GC Roots

在可达性分析算法中提到了 GC Roots 这个概念,那么在 Java 中,有哪些对象可以被作为 GC Roots 呢?分别有如下几种情况。

  1. 虚拟机栈中每个栈帧中局部变量表里面的引用对象,如方法的入参,局部变量等。
  2. 本地方法栈中的引用对象。
  3. 方法区中类的静态属性引用的对象。
  4. 方法区中常量池引用的对象,如:字符串常量池引用的对象。
  5. 被关键字 synchronized 锁住的对象。
  6. Java 虚拟机内部引用的对象,如:一些常驻的异常对象(NullPointerException、OutOfMemoryError),基本数据类型的 Class 对象,系统类加载器等。
  7. 反应 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
  8. 除了这些固定的 GC Roots 外,根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象”临时性“地加入,共同构成完整的 GC Roots 集合,比如:分代收集器和局部回收。

总结

本文主要介绍了 JVM 垃圾回收的作用区域,以及如何判断一个对象是否是垃圾,通常可以通过引用计数法和可达性分析算法来判断一个对象是否是垃圾,但是在目前 JVM 的垃圾收集器中,采用的都是可达性分析算法,因为引用计数法无法解决循环依赖的问题。最后列举了在可达性分析算法里,Java 中哪些对象可以作为 GC Roots。

垃圾回收通常会分为两个阶段:垃圾标记阶段和垃圾清除阶段。而引用计数算法和可达性分析算法作用的是垃圾标记阶段,后面的文章将会分享垃圾清除阶段的相关算法。

参考

  • 周志明《深入理解 Java 虚拟机》第三版。
垃圾回收之标记算法