虚拟机如何确定一个对象是不是垃圾

引用计数算法

使用一个引用计数器记录该对象还有多少个引用指针指向该对象,此算法简单高效但需要在代码中进行额外的逻辑处理以防止循环引用导致内存泄露的问题。

让我们来一起看看下面的例子来理解循环引用内存泄漏两个概念:

/**
 * @author Zeng
 * @date 2020/4/6 11:41
 */
public class ReferenceCountingGC {

    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 占点内存,以便观察清楚GC日志中是否有进行垃圾收集
     */
    private byte[] bigSize = new byte[2 * _1MB];
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
    }
    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        //对象内部的instance引用指针相互指向对方实例
        objA.instance = objB;
        objB.instance = objA;
		//除了instance指针以外没有其它引用指针指向这两个对象
        objA = null;
        objB = null;
        System.gc();
        //执行finalize()方法的速度有些慢,让主线程等待一下它执行
        Thread.sleep(500);
    }
    public static void main(String[] args) {
        testGC();
    }
}
循环引用

在引用计数算法中对于objAobjB是无法回收的,因为它们内部含有对方实例的引用指针,但是除此之外没有其它指针引用这两个对象,也无法访问到这两个对象,JVM无法回收这两个对象,这就导致了内存泄漏

执行结果如下:

执行结果

可以看到JVM里面并不是采用引用计数算法,因为在显式指定垃圾收集时JVM确实把这两个对象给回收了,这两个对象的finalize()方法被调用了,这个方法是当对象第一次被回收时被调用的。

那么JVM是如何确定对象是不是一个“垃圾”呢?

对象可达性分析

通过一系列的“GC Roots”根对象作为起始节点,一直往下搜索引用关系,搜索过程所走过的路径称为“引用链”。

看下面的代码可以构成一条引用关系链,而objD因为没有指针引用它而成为了垃圾,等待下一次垃圾回收来了结它。

/**
 * @author Zeng
 * @date 2020/4/7 7:44
 */
public class ReferenceList {

    private String name;
    public Object instance = null;
    
    public ReferenceList(String name) {
        this.name = name;
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println(name + " finalize method executed!");
    }
    
    public static void main(String[] args) throws InterruptedException {
        ReferenceList objA = new ReferenceList("objA");
        ReferenceList objB = new ReferenceList("objB");
        ReferenceList objC = new ReferenceList("objC");
        objA.instance = objB;
        objB.instance = objC;
        objB = null;
        objC = null;
        ReferenceList objD = new ReferenceList("objD");
        objD = null;
        System.gc();
        Thread.sleep(500);
    }

}

上面代码得到的引用关系链如下图所示:

引用关系链

所以objD会被JVM回收,而objA、objB、objC可以存活下来,如下图所示可以验证这个结果:

objD对象被回收

固定作为GC Roots的对象主要有以下几种:

虚拟机栈中引用的对象,例如局部变量形式参数临时变量······

类静态属性引用的变量,例如类的静态引用类型成员变量

常量引用的对象,例如String str = "alive"字符串常量池中的引用

同步锁引用的对象,例如synchronized(obj)中被锁住的obj可作为GC Roots

细分的四种引用类型指定对象被回收的时机

强引用(Strongly Reference):即类似于Object objA = new Object()这种引用关系,垃圾收集器是永远不会回收掉强引用的对象的。

软引用(Soft Reference):用于表示还有用,但非必须的对象,只有在发生内存溢出之前,才会回收这类对象。

弱引用(Weak Reference):被弱引用关联的对象只能存活到下一次垃圾回收发生为止,无论内存是否可用,都会回收该类对象。

虚引用(Phantom Reference):最差劲的一种引用关系,无法通过虚引用获取对象实例,设置虚引用的目的就是为了让它成为垃圾被回收掉

下面例子可以证明它们引用回收的时机

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

/**
 * @author Zeng
 * @date 2020/4/7 8:16
 * 四种引用类型实践
 */
public class Reference {

    private String name;
    public Reference(String name) {
        this.name = name;
    }
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println(name + " finalize method executed!");
    }
    public static void main(String[] args) throws InterruptedException {
        //强引用
        Reference obj = new Reference("强引用obj");
        System.gc();
        Thread.sleep(500);
        //软引用
        Reference objA = new Reference("软引用objA");
        SoftReference<Reference> softReferenceA = new SoftReference<>(objA);
        objA = null;
        System.gc();
        Thread.sleep(500);
        //弱引用
        Reference objB = new Reference("弱引用objB");
        WeakReference<Reference> weakReferenceB = new WeakReference<>(objB);
        objB = null;
        System.gc();
        Thread.sleep(500);
        //虚引用
        Reference objC = new Reference("虚引用objC");
        ReferenceQueue<Reference> phantomQueue = new ReferenceQueue<>();
        PhantomReference<Reference> phantomReference = new PhantomReference<>(objC, phantomQueue);
        objC = null;
        System.gc();
        Thread.sleep(500);
    }
}

运行结果:

运行结果

可以看到弱引用和虚引用在下一轮垃圾回收时都会被当成垃圾给回收掉,而强引用和软引用没有被回收,如果构造出堆溢出的情况,软引用也会被回收。

总结

本文主要讲解了JVM如何确定一个对象是否可以回收,以及介绍引用计数法和可达性分析两种方法判断对象是否可回收,最后通过Java的四种引用类型的实践验证了强引用、软引用、弱引用和虚引用的垃圾回收时机,如果有任何错误欢迎提出,乐意与大家交流学习!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐