zl程序教程

您现在的位置是:首页 >  Python

当前栏目

Java finalize函数与软引用、弱引用、虚引用

2023-03-15 22:01:24 时间

一、finalize函数的作用

       它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协”。也就是说,finalize函数最初被设计的用途是类似于C/C++的析构函数,用于在对象被销毁前最后的内存回收。Java与C/C++的相似性和不同之处在于:在C++中,对象的内存在哪个时刻被回收,是可以明确确定的(假设程序没有缺陷),一旦C++的对象要被回收了,在回收该对象之前对象的析构函数将被调用,在该函数中释放对象占用的内存;在java中,对象的内存在哪个时刻回收,取决于垃圾回收器何时运行,一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正的回收对象占用的内存,由于JVM垃圾回收运行时机是不确定的,因而finalize()的调用具有不确定性。JVM只保证方法会调用,但不保证方法里的任务会被执行完(这块儿可以从Java源码Finalizer.class中得知:在源码中,执行finalize()方法是通过开启一个低优先级的线程来执行的,而finalize()方法在执行过程中的任何异常都会被catch,然后被忽略,因而无法保证finalize方法里的任务会被执行完)由于执行finalize()的是一个低优先级的线程,既然是一个新的线程,虽然优先级低了点,但也是和垃圾收集器并发执行的,所以垃圾收集器没必要等这个低优先级的线程执行完才继续执行。也就是说,有可能会出现对象被回收之后,那个低优先级的线程才执行finalize()方法。

        也就是说,在C++中,我们依赖析构函数来实现资源释放,但我们却不能指望finalize()方法来实现资源释放。所以建议就是不要指望finalize函数来实现资源释放,而是主动在代码中显式释放相关资源。不过,尽管不建议使用该函数,但这并不妨碍我们理解该函数在JVM垃圾回收过程中被调用的时机。我们知道,finalize()方法是Object类中的一个方法体为空的方法,而我们创建的所有类默认都继承Object类,因而只有当我们在自定义类中覆写了该方法时,JVM在回收我们定义的类的时候,才会调用finalize函数,而只有JVM需要调用finalize函数时,它才需要执行两次垃圾回收来销毁我们定义的类。也就是说,如果我们定义的类中没有覆写该方法,那么垃圾回收只需要执行一次就可以了。垃圾回收的具体机制是怎么样的呢?

        当我们定义的类覆写了finalize方法后,在该类的初始化过程中,这个类的对象会被包装成一个java.lang.ref.Finalizer并添加到Finalizer类的静态链表unfinalized中。当执行第一次垃圾回收时,发现该对象具有finalize方法且没被执行过,因而这个对象不会被回收,而是从unfinalized链表中移除,然后添加到Finalizer类的静态引用队列queue中。查看Finalizer源码可以看到一个内部类:java.lang.ref.Finalizer.FinalizerThread,该类就是用于监视Finalizer的引用队列queue的。当它发现queue队列的变化,就依次将队列中的对象移除,并调用该对象的finalize()函数。当执行第二次垃圾回收时,发现该类虽然覆写了finalize方法,但已经执行过了,就可以直接将该类回收。以上是覆写了finalize函数的类的回收过程。对于没有覆写finalize函数的类或者已经执行过一次finalize函数的类,在垃圾回收时更简单,直接被回收即可。这里finalize函数只会被执行一次的原因是防止类在执行finalize函数时将该类复活,从而导致该类永远无法被回收。

二、软引用、弱引用、虚引用

        这里不会具体介绍这三种引用的含义,有需要的可自行百度。这里重点介绍下设计这三种引用的不同目的。

         软引用:软引用是用来描述非必需对象的,软引用常常被用来实现内存敏感的高速缓存,原因是垃圾回收器不会轻易回收存在软引用的对象,只有当内存不足时垃圾收集线程才会对软引用对象进行回收。

        弱引用:弱引用也是用来描述非必需对象的,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

        虚引用:虚引用也称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收后收到一个系统通知

        引用队列:在创建对象的软引用、弱引用或虚引用的时候,可以将引用对象和一个引用队列关联起来,当垃圾收集器决定对这些对象进行垃圾回收的时候,就会将引用对象添加到所关联的引用队列。通过开启线程监听该引用队列的变化情况就可以在对象被回收时采取相应的动作。由于虚引用的唯一目的就是能在这个对象被垃圾收集器回收后能收到系统通知,因而创建虚引用时必须要关联一个引用队列,而软引用和弱引用则不是必须的。这里所谓的收到系统通知其实还是通过开启线程监听该引用队列的变化情况来实现的。这里还需要强调的是,对于软引用和弱引用,当执行第一次垃圾回收时,就会将软引用或弱引用对象添加到其关联的引用队列中,然后其finalize函数才会被执行(如果没覆写则不会被执行);而对于虚引用,如果被引用对象没有覆写finalize方法,则是在第一垃圾回收将该类销毁之后,才会将虚拟引用对象添加到引用队列,如果被引用对象覆写了finalize方法,则是当执行完第二次垃圾回收之后,才会将虚引用对象添加到其关联的引用队列。下面用实验来证明这一点。

1、覆写finalize方法的对象回收

下面的代码中测试了覆写了finalize方法的TestClass类对象被垃圾回收时虚引用及弱引用队列的变化过程。代码中之所以要执行sleep语句是为了保证垃圾回收被执行,如果不执行sleep语句,则无法得到预期的结果。

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

public class TestPhantomReference {

    public static void main(String[] args) {
        ReferenceQueue phantomRQ = new ReferenceQueue();//虚引用队列
        ReferenceQueue weakRQ = new ReferenceQueue();//弱引用队列
        TestClass testClass = new TestClass();//被回收对象
        PhantomReference pr = new PhantomReference(testClass, phantomRQ);//虚引用
        WeakReference wr = new WeakReference(testClass, weakRQ);//弱引用
        System.out.println("pr: before gc: " + pr.get() + ", " + phantomRQ.poll());
        System.out.println("wr: before gc: " + wr.get() + ", " + weakRQ.poll());
        testClass = null;//去掉强引用
        System.gc();//执行第一次gc
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("pr: after first gc: " + pr.get() + "," + phantomRQ.poll());
        System.out.println("wr: after first gc: " + wr.get() + "," + weakRQ.poll());
        System.gc();//执行第二次gc
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("pr: after second gc: " + pr.get() + "," + phantomRQ.poll());
        System.out.println("wr: after second gc: " + wr.get() + "," + weakRQ.poll());
    }
}

class TestClass {//被回收对象的类覆写了finalize方法
    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed");
    }
}

执行结果如下:

pr: before gc: null, null
wr: before gc: TestClass@279f2327, null
finalize method executed
pr: after first gc: null,null
wr: after first gc: null,java.lang.ref.WeakReference@2ff4acd0
pr: after second gc: null,java.lang.ref.PhantomReference@54bedef2
wr: after second gc: null,null

为了方便观察,我们去掉finalize method executed这行,并将执行结果调整一下顺序:

pr: before gc: null, null
pr: after first gc: null,null
pr: after second gc: null,java.lang.ref.PhantomReference@54bedef2

wr: before gc: TestClass@279f2327, null
wr: after first gc: null,java.lang.ref.WeakReference@2ff4acd0
wr: after second gc: null,null

从上面的结果可以看到,对于弱引用队列,在执行第一次gc之后,从队列中poll到的引用对象不为空,说明确实在第一次gc之后,弱引用对象被添加进了弱引用队列;相反,对于虚引用,第一次gc之后,从虚引用队列中获取的对象为null,直到第二次gc才从虚引用队列中获取到了虚引用对象。

2、未覆写finalize方法的对象回收

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


public class TestPhantomReference {

    public static void main(String[] args) {
        ReferenceQueue phantomRQ = new ReferenceQueue();//虚引用队列
        ReferenceQueue weakRQ = new ReferenceQueue();//弱引用队列
        StringBuilder builder = new StringBuilder();//被回收对象
        PhantomReference pr = new PhantomReference(builder, phantomRQ);//虚引用
        WeakReference wr = new WeakReference(builder, weakRQ);//弱引用
        System.out.println("pr: before gc: " + pr.get() + ", " + phantomRQ.poll());
        System.out.println("wr: before gc: " + wr.get() + ", " + weakRQ.poll());
        builder = null;//去掉强引用
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("pr: after first gc: " + pr.get() + "," + phantomRQ.poll());
        System.out.println("wr: after first gc: " + wr.get() + "," + weakRQ.poll());
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("pr: after second gc: " + pr.get() + "," + phantomRQ.poll());
        System.out.println("wr: after second gc: " + wr.get() + "," + weakRQ.poll());
    }
}

上述代码的执行结果如下:

pr: before gc: null, null
wr: before gc: , null
pr: after first gc: null,java.lang.ref.PhantomReference@279f2327
wr: after first gc: null,java.lang.ref.WeakReference@2ff4acd0
pr: after second gc: null,null
wr: after second gc: null,null

我们可以看到,由于StringBuilder没有覆写finalize方法,无论是虚引用队列还是弱引用队列,都是在第一次垃圾回收之后就被添加进了各自的引用队列。这里需要再补充说明的是,在第一次垃圾回收之后,被添加进引用队列的是引用对象(Reference),而引用对象指向的对象(StringBuilder)已经被回收了。

参考博客:

1、https://blog.csdn.net/yizishou/article/details/71194944  弱引用、虚引用、finalize实践,及它们的顺序

2、https://blog.csdn.net/qiaoguaping9272/article/details/82078643  深入理解JVM(三)------再谈引用与finalize()方法

3、https://www.jianshu.com/p/e5364c05cc80  通过例子理解java强引用,软引用,弱引用,虚引用

4、https://blog.csdn.net/a4171175/article/details/90749839  finalize()

5、https://blog.csdn.net/Mark2When/article/details/59162810 Java回收对象的标记 和 对象的二次标记过程

6、https://www.jianshu.com/p/651e7011c018 FinalReference