vlambda博客
学习文章列表

JAVA垃圾回收机制(GC)--标记算法

1、 概述

Java的堆内存里面几乎存放着所有的对象实例,垃圾收集器在对堆进行回收之前,需要判断哪些对象是存活的,哪些对象已经死去,因此需要规则去判断哪些是可以回收的,哪些是不能回收的。本文涉及两个算法,引用计数器算法和可达性分析算法。

2、引用计数器算法

2.1、概念

引用计数器算法比较简单,通过在对象头中分配一个空间来保存该对象被引用的次数,如果该对象被引用,则其引用次数加一;如果删除该对象的引用,则其引用次数减一,当该对象的引用次数为零时,则该对象会被回收。

 String p = new String("abc");

如上,当创建String对象p时,abc对象的引用计数值加1为1.

而当我们去除abc字符串对象的引用时,则abc字符串对象的引用计数减1为0.

p = null;

JAVA垃圾回收机制(GC)--标记算法

2.2、实现原理

看如下伪代码:


New(): //分配内存
   ref <- allocate()
   if ref == null
       error "Out of memory"
   rc(ref) <- 0  //将ref的引用计数(reference counting)设置为0
   return ref
atomic Write(dest, ref) //更新对象的引用
   addReference(ref)
   deleteReference(dest)
   dest <- ref
addReference(ref):
   if ref != null
       rc(ref) <- rc(ref)+1
       
deleteReference(ref):
   if ref != null
       rc(ref) <- rc(ref) -1
       if rc(ref) == 0 //如果当前ref的引用计数为0,则表明其将要被回收
           for each fld in Pointers(ref)
               deleteReference(*fld)
           free(ref) //释放ref指向的内存空间
  • 当对象的引用发生变化时,比如说将对象重新赋值给新的变量等,对象的引用计数如何变化。假设我们有两个变量p和q,它们分别指向不同的对象,当我们将他们指向同一个对象时,下面的图展示了p和q变量指向的两个对象的引用计数的变化。

    String p = new String("abc")
    String q = new String("def")
    p = q

    当我们执行代码p=q时,实际上相当于调用了伪代码中的Write(p,q), 即对p原先指向的对象要进行deleteReference()操作 - 引用计数减一,因为p变量不再指向该对象了,而对q原先指向的对象要进行addReference()操作 - 引用计数加一。

JAVA垃圾回收机制(GC)--标记算法

  • 当某个对象的引用计数减为0时,collector需要递归遍历它所指向的所有域,将它所有域所指向的对象的引用计数都减一,然后才能回收当前对象。在递归过程中,引用计数为0的对象也都将被回收,比如说下图中的phone和address指向的对象。

    JAVA垃圾回收机制(GC)--标记算法

2.3、优缺点

  • 优点

    1. 实时:无需等到内存不够时进行回收,运行时根据数据对象的计数器是否为0,为0就可以直接回收;

    2. 应用无需挂起:垃圾回收时,应用无需挂起。如果申请内存时,内存不足则会立刻报OOM异常;

    3. 区域性:更新对象计数时,只会影响到该对象,不会去扫描全部对象。

  • 缺点

    1. 每次对象被引用时,都需要去更新计数器,有时间开销,尤其是在循环时;

    2. 无法解决循环依赖的问题

public class Test {

   public static void main(String[] args) {

       A a = new A();
       B b = new B();

       a.setB(b);
       b.setA(a);

  }
}

class A {
   private B b;

   public B getB() {
       return b;
  }

   public void setB(B b) {
       this.b = b;
  }
}

class B {
   private A a;

   public A getA() {
       return a;
  }

   public void setA(A a) {
       this.a = a;
  }
}

2.4、应用

Python虚拟机、redis、OkHttp、netty等都有使用到这种算法

3、根搜索算法

3.1、概念

以gc roots对象开始,到指定对象是否可达。若gc roots不可达(当一个对象到gc roots没有任何引用链相连),则判定此对象为可回收对象。

根搜索算法判断对象的存活与对象的引用有关,之前有记录过Java的四种引用类型(强引用、软引用、弱引用、虚引用),在实际回收标记中,根搜索算法判断的对象不可达并非直接判定对象”死亡“,这时候的对象处于”缓刑“阶段,真正进行回收至少需要经历至少两次标记阶段,

如果一个对象通过根搜索之后发现没有与GC Roots相连的引用连接,此时会进行第一次标记并且进行筛选,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则都不会再执行。此时对象没有自救的机会。

如果对象被判断为有必要执行finalize()方法,此时对象会被放在名为F-Queued的队列中,并在稍后由虚拟机自动创建的低优先级的Finalize线程去执行,这里所谓的执行指的是虚拟机会触发这个方法,但并保证会等待进行结束。

finalize()方法是对象逃脱”死亡“的最后一次机会,且只有一次。稍后GC将会对F-Queued中的对象进行第二次小规模的标记,如果对象想要在finalize()中成功拯救自己,只要重新引用链接上任何一个对象建立链接关系即可,譬如把自己(this关键字)赋值给某个变量或者对象的成员变量,则此对象再第二次标记时将会被移出”即将回收“的集合。

如下代码:

public class FinalizeEscapeGC {

   private static FinalizeEscapeGC SAVE_HOOK = null;

   private void isAlive() {
       System.out.println("Yes,I am still alive!");
  }

   @Override
   protected void finalize() throws Throwable {
       super.finalize();
       System.out.println("finalize() method executed!");
       FinalizeEscapeGC.SAVE_HOOK = this;
  }

   public static void main(String args[]) throws Throwable {
       SAVE_HOOK = new FinalizeEscapeGC();
       // 对象第一次成功拯救自己
       SAVE_HOOK = null;
       // 调用该方法建议系统执行垃圾清理,但也并不一定执行
       System.gc();
       // 因为Finalize线程优先级较低,暂停0.5秒以等待它
       Thread.sleep(500);
       if (SAVE_HOOK != null) {
           SAVE_HOOK.isAlive();
      } else {
           System.out.println("No, I am dead!");
      }
       // 下面这段代码与上面完全相同,但这次却自救失败了
       SAVE_HOOK = null;
       System.gc();
       Thread.sleep(500);
       if (SAVE_HOOK != null) {
           SAVE_HOOK.isAlive();
      } else {
           System.out.println("No, I am dead!");
      }
  }
}

3.2、GC Roots

3.2.1、概念

可达性算法的原理是以一系列叫做  GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点。这样通过 GC Root 串成的一条线就叫引用链),直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。

如图示,如果用可达性算法即可解决上述循环引用的问题,因为从GC Root 出发没有到达 a,b,所以 a,b 可回收。

3.2.2、两栈两方法

在Java中,哪些可以作为GC Root呢,归结为两栈两方法

  1. 两栈

    • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  2. 两方法

    • 方法区中类静态属性引用的对象

    • 方法区中常量引用的对象

3.2.2.1、虚拟机栈中引用的对象

如下代码所示,a 是栈帧中的本地变量,当 a = null 时,由于此时 a 充当了 GC Root 的作用,a 与原来指向的实例 new Test() 断开了连接,所以对象会被回收。

public class Test {
   public static  void main(String[] args) {
Test a = new Test();
a = null;
  }
}
3.2.2.2、方法区中类静态属性引用的对象

所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并非 Java 实现的,可能由 C 或 Python等其他语言实现的, Java 通过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。通过调用本地的库文件的内部方法,使 JAVA 可以实现和本地机器的紧密联系,调用系统级的各接口方法,还是不明白?见文末参考,对本地方法定义与使用有详细介绍。

当调用 Java 方法时,虚拟机会创建一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
  // 缓存String的class
  jclass jc = (*env)->FindClass(env, STRING_PATH);
}

如上代码所示,当 java 调用以上本地方法时,jc 会被本地方法栈压入栈中, jc 就是我们说的本地方法栈中 JNI 的对象引用,因此只会在此本地方法执行完成后才会被释放。

3.2.2.3、方法区中类静态属性引用的对象

如下代码所示,当栈帧中的本地变量 a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收,而由于我们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的作用,它指向的对象依然存活!

public class Test {
   public static Test s;
   public static  void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
  }
}
3.2.2.4、方方法区中常量引用的对象

如下代码所示,常量 s 指向的对象并不会因为 a 指向的对象被回收而回收


public class Test {
public static final Test s = new Test();
       public static void main(String[] args) {
   Test a = new Test();
   a = null;
      }
}