本文最后更新于:2020年9月6日 上午

JVM系列(四)-GC如何判断对象是否存活

昨天在和项目组朋友交流的过程中,发现一部分的知识点又有点记不清楚了,于是今天赶紧写下了这篇博客,记录下自己的学习理解。

首先,我们需要认识到这一部分是属于垃圾回收(Garbage Collection,GC)中的内容。


1、学习GC需要思考的三个问题

那么对于垃圾回收我们肯定会考虑下面3个问题:

  • 什么对象需要进行垃圾回收?(Why)
  • 什么时候会进行垃圾回收?(When)
  • 垃圾回收是如何进行的?(How)

在思考这三个问题前,我们需要认识到,我们的GC虽然是比较自动化的,但是这些自动化的进行,有时会出现瓶颈,这也就是为什么我们会去研究垃圾收集和内存分配的原因。


2、不同数据区域是如何进行垃圾回收的

  • 程序计数器、虚拟机栈、本地方法栈

这三个区域是线程私有的,随线程生死。分配给栈帧的内存可以认为是编译期可知的,具有确定性,所以方法和线程结束时,内存会自然被回收。

  • Java堆、方法区

这部分的区域的内存分配和垃圾回收具有不确定性,因为这个部分的内存,只有在运行期间才能确定,所以垃圾回收是动态的。


3、对象是否存回

那么回到本篇的标题,我们本篇主要讨论的是对象是否存活,也就是问题中的第一个问题,什么对象需要进行垃圾回收。

如何判断对象是否存活的算法:

  • 引用计数算法
  • 可达性分析算法

引用计数算法

引用计数算法的实现方式

在对象中添加计数器,有地方引用它,计数器的值就+1,如果引用失效,就-1,计数器的值为0时,这个对象将不能被使用。

引用计数算法的问题

虽然它的原理简单,判定效率高,但是它很难解决对象之间的相互循环引用的问题,他们相互引用,就会导致他们的引用计数都不为零,导致无法回收他们。

简单表示为:

objA.instance = objB
objB.instance = objA

可达性分析算法

首先我们需要认识到,在Java中我们使用的是可达性分析算法。

可达性分析算法的实现方式

通过一系列称为“GC Roots”的根对象作为起始节点集,然后根据引用关系开始向下搜索,如果某个对象没有和GC Roots相连,那么就说明这个对象不能被使用。

固定作为GC Roots的对象

  • 虚拟机栈(栈帧中的本地变量表)引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部引用(基本数据类型对应的Class对象,异常对象,系统类加载器)
  • 所有被同步锁持有的对象

4、引用

我们可以发现我们的引用计数算法和可达性分析算法,都谈到了对对象的引用,那么什么是引用了?

引用是什么

狭义的描述:引用就是reference类型数据中存储的数值,是另一块内存的起始地址。(这样描述的话只存在引用和被引用的关系)

引用的分类

引用可以分为四个类别:

  • 强引用
  • 弱引用
  • 虚引用
  • 软引用

强引用是什么

强引用指代码中普遍存在的引用,即Object obj = new Object()这种关系。

注意:强引用不会被GC回收。

软引用是什么

用来描述一些还有用,但是非必须的对象。在JDK1.2提供了SoftReference类实现软引用。

注意:软引用会在发生OOM的时候,才会进行回收,如果回收后的空间仍然不够,会抛出OOM的异常

弱引用是什么

用来描述那些非必须对象。在JDK1.2提供WeakReference类来描述弱引用。

注意:弱引用关联的对象只能生存到下一次GC发生。

虚引用

最弱的引用关系。在JDK1.2提供了PhantomReference类实现虚引用。

注意:虚引用的目的只是为了在GC回收的时候获取一个系统通知。

5、对象是生还是死

可达性分析算法中对象的生死判断

首先我们需要明白的一点是,在可达性分析算法中,不可达的对象不是一定是死亡的。

它需要进行两次标记的过程!


可达性分析算法中标记的过程

  • 第一次标记

某个对象与GC Roots不可达时,会第一次被标记。

  • 筛选

是否执行finalize()方法。

- 没有必要执行:
    1.对象没有覆盖finalize()方法
    2.finalize已经被虚拟机调用过了
- 有必要执行:
    将对象放入F-Queue队列中。
  • 第二次标记

第二次标记就是对F-Queue队列中的对象进行标记。


如何在标记过程中拯救自己

只需要在第二次标记的时候重新与引用链关联起来,这样就可以被移除F-Queue。

注意:这种自救的机会只有一次,因为对象的finalize()方法最多只有被系统自动调用一次。
public class GCDemo {
    private static GCDemo gcDemo = new GCDemo();
    @Override
    protected void finalize() throws Throwable {
        System.out.println("尝试自救");
        gcDemo = this;
        System.out.println("自救成功");
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        gcDemo = null;
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(gcDemo);
        gcDemo = null;
        System.gc();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(gcDemo);
    }
}
注意:不推荐使用这种方式去拯救对象。

6、回收方法区

主要回收内容

  • 废弃的常量
  • 不再使用的类型

如何判断是废弃的常量

当没有任何一个对象引用常量池中的这个常量时,就会被系统清除出常量池。

如何判断是不再使用的类型

判断是不是不再使用的类型,被允许被回收需要满足3个条件:

  • 该类所有的实例已经被回收。(堆中没有该类和子类的实例)
  • 加载该类的类加载器已经被回收。
  • 该类对象的java.lang.Class对象没有在任何地方被引用。(无法反射访问该类的方法)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!博客中转载文章会注明出处,若有版权问题,请及时与我联系!谢谢!

计算机网络系列(一)-计算机网络简介 上一篇
Java并发系列(七)-Java内存模型-基础 下一篇