Java中的四种引用类型及其使用方式

Java中有四种引用类型,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(WeakReference)、虚引用(PhantomReference)。

为什么要将引用分成这四种类型?

这要从Java管理内存的方式说起。Java为了将程序员从内存管理中解救出来,即不让程序员自己申请堆内存(比如C语言程序员需要通过malloc请求操作系统分配一块堆内存给自己使用),自己去释放堆内存(比如C语言程序员需要通过free来释放内存),降低程序员的心智负担,且可以增加开发效率。Java虚拟机通过追踪Java中的对象是否有来自GC Roots的强引用指向它来决定对象所占用的堆内存是否可以回收。

强引用

我们平常写的代码,比如:

String abc = new String();
List intList = new ArrayList<>();

其中的引用abc和intList都是强引用,也就是说引用默认都是强引用。

软引用

软引用是什么样的呢?如果我们想用软引用,需要通过java.lang.ref.SoftReference,比如:

SoftReference softRef = new SoftReference<>(new YourObject());

这样,softRef这个强引用指向的SoftReference对象中保存的就是一个软引用,这儿就是YourObject对象的软引用。如图:

如果我们想要获取软引用指向的对象,可以通过SoftReference对象的get方法获取,比如:

YourObject yourObj = softRef.get();

注意,软引用是为了告诉Java虚拟机,如果内存不足的时候,你可以把我指向的对象回收掉。也就是说,即使SoftReference对象仍然可以通过GC Roots访问到,但是它内部的软引用指向的对象仍然可以被回收。

我们举个例子:

package references;
import java.lang.ref.SoftReference;
/**
 * @author pilaf
 * @description
 * @date 2023-04-05 21:03
 **/
public class SoftReferenceDemo { public static class DataBlock { private final byte[] bytes;
        public DataBlock(int byteCount) { // 创建DataBlock对象的时候,将会在堆上分配一个byteCount个字节的数组
            bytes = new byte[byteCount];
        }
        public String toString() { return "DataBlock(byteCount=" + bytes.length + ")";
        }
    }
    public static void main(String[] args) { // 1024*1024*10字节是10MB
        SoftReference softReference = new SoftReference<>(new DataBlock(1024 * 1024 * 10));
        System.out.println("通过软引用访问到的对象:" + softReference.get());
        System.out.println("通过软引用访问到的对象:" + softReference.get());
    }
}

通过配置JVM运行参数,指定-Xms10m -Xmx10m,我们让堆大小为10MB:

运行上面的代码,控制台输出:

可见,可以通过软引用访问到我们的DataBlock对象。

我们还可以再两次调用的System.out.println(“通过软引用访问到的对象:” + softReference.get());中间加上:

System.gc(); 

会发现第二次仍然可以拿到软引用指向的对象:

让我们再做点小改动,将main方法的方法体改成下面:

 // 1024*1024*10字节是10MB
        SoftReference softReference = new SoftReference<>(new DataBlock(1024 * 1024 * 10));
        System.out.println("通过软引用访问到的对象:" + softReference.get());
        // 创建一个10MB的数组
        byte[] anotherByteArray = new byte[1024 * 1024 * 10];
        System.out.println("通过软引用访问到的对象:" + softReference.get());

即增加了一行创建anotherByteArray数组的代码,再次运行main方法:

看到了么,当我们尝试再在堆上分配一个10MB的数组的时候,堆内存会不够用(因为我们前面配置了堆内存为10MB),这个时候JVM会回收掉软引用指向的对象,当我们再次调用softReference.get()来获取softReference对象内部保存的软引用指向的对象的时候,发现它已经是null了,即被回收了。

弱引用

弱引用和软引用的用法一样,只不过我们需要通过java.lang.ref.WeakReference对象来包着一个弱引用。但是弱引用的更脆弱,只要垃圾回收器回收内存垃圾的时候,不管内存是否够用,都会回收掉弱引用指向的对象,比如下面的程序:

package references;
import java.lang.ref.WeakReference;
/**
 * @author pilaf
 * @description
 * @date 2023-04-05 21:41
 **/
public class WeakReferenceDemo { public static void main(String[] args) { WeakReference weakReference = new WeakReference<>(new String("abc"));
        System.out.println("通过弱引用访问到的对象:" + weakReference.get());
        System.gc(); // 触发垃圾回收
        System.out.println("通过弱引用访问到的对象:" + weakReference.get());
    }
}

运行的结果:

可见,只要经过垃圾回收,弱引用指向的对象都会被回收,不管堆内存是否够用。

JDK中的ThreadLocal内部的ThreadLocalMap就用到了弱引用来避免内存泄漏:

虚引用

虚引用主要用于在垃圾回收器回收完虚引用指向的对象后,允许我们做一些资源释放的工作(比如释放一些堆外内存)。

让我们先模仿着前面的软引用和弱引用的方式使用虚引用:

package references;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * @author pilaf
 * @description
 * @date 2023-04-05 22:00
 **/
public class PhantomReferenceDemo { private static class MyClass{ private Date birthTime;
        public MyClass(){ birthTime = new Date();
        }
    }
    public static void main(String[] args) { // 如果PhantomReference构造器的第二个参数java.lang.ref.ReferenceQueue传递null,那么永远也无法获取到虚引用指向的对象了
        PhantomReference phantomReference = new PhantomReference<>(new MyClass(), null);
        // PhantomReference的get方法总是返回null,因为虚引用指向的对象总是无法访问的。
        System.out.println("phantomReference.get:"+phantomReference.get());
    }
}

运行后控制台输出:

phantomReference.get:null

因为虚引用的get方法永远返回null。

我们应该通过下面这样的方式使用虚引用:

package references;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
 * @author pilaf
 * @description
 * @date 2023-04-05 22:00
 **/
public class PhantomReferenceDemo { private static class MyClass{ private Date birthTime;
        public MyClass(){ birthTime = new Date();
        }
    }
    public static void main(String[] args) throws Exception{ ReferenceQueue queue = new ReferenceQueue<>();
        // 虚引用需要和引用队列一起使用,这样再垃圾回收完虚引用的对象后,它的虚引用会被放到队列中
        PhantomReference phantomReferenceWithQueue = new PhantomReference<>(new MyClass(), queue);
        // 启动另一个线程来检查是否有虚引用被回收了
        new Thread(()->{ while (true){ Reference ref = queue.poll();
                if (ref!=null){ System.out.println("虚引用被回收:" + ref);
                }
            }
        }).start();
        // 稍微睡眠一下,确保前面的线程启动了
        TimeUnit.SECONDS.sleep(3);
        // 暗示JVM进行垃圾回收
        System.gc();
        
    }
}

运行后控制台输出:

虚引用被回收:java.lang.ref.PhantomReference@591f096f

可见,在一个线程中不断去检测引用队列,可以拿到被垃圾回收的虚引用的对象的引用,从而可以进行资源的释放。

一般情况下,都是用自定义的资源释放类来继承虚引用,比如下面的例子:

package references;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
 * @author pilaf
 * @description
 * @date 2023-04-05 22:00
 **/
public class PhantomReferenceDemo { private static class MyClass{ private Date birthTime;
        public MyClass(){ birthTime = new Date();
        }
        // 不能重写finalize方法,否则MyResourceFinalizer就不会被放到引用队列中
        // 原因见:https://stackoverflow.com/questions/48167735/why-the-reference-dont-put-into-reference-queue-when-finalize-method-overrided
//        @Override
//        public void finalize() throws Throwable{//            System.out.println("finalize invoked..");
//        }
    }
    private static class MyResourceFinalizer extends PhantomReference { // 模拟要释放的内存地址
        private String toReleaseAddress = null;
        public MyResourceFinalizer(MyClass referent,ReferenceQueue referenceQueue) { super(referent,referenceQueue);
            toReleaseAddress = String.valueOf(referent.hashCode());
        }
        public void releaseResource(){ System.out.println("释放内存地址:" + toReleaseAddress);
        }
    }
    public static void main(String[] args) throws Exception{ ReferenceQueue queue = new ReferenceQueue<>();
        List myClassList = new ArrayList<>();
        List myResourceFinalizers = new ArrayList<>();
        for (int i = 0; i < 5; i++) { MyClass myClass = new MyClass();
            myClassList.add(myClass);
            // 虚引用需要和引用队列一起使用,这样再垃圾回收完虚引用的对象后,它的虚引用会被放到队列中
            myResourceFinalizers.add(new MyResourceFinalizer(myClass, queue));
        }
        // 启动另一个线程来检查是否有虚引用被回收了
        new Thread(()->{ while (true){ MyResourceFinalizer ref = (MyResourceFinalizer)queue.poll();
                if (ref!=null){ System.out.println("虚引用被回收:" + ref);
                    ref.releaseResource();
                    ref.clear();
                }
            }
        }).start();
        // 稍微睡眠一下,确保前面的线程启动了
        TimeUnit.SECONDS.sleep(2);
        // help gc
        myClassList = null;
        // 暗示JVM进行垃圾回收
        System.gc();
        for (MyResourceFinalizer myResourceFinalizer : myResourceFinalizers) { // 输出true,才表示引用进了队列了
            System.out.println(myResourceFinalizer+" isEnqueued:"+myResourceFinalizer.isEnqueued());
        }
    }
}

运行完控制台输出:

references.PhantomReferenceDemo$MyResourceFinalizer@2d98a335 isEnqueued:true
references.PhantomReferenceDemo$MyResourceFinalizer@16b98e56 isEnqueued:true
references.PhantomReferenceDemo$MyResourceFinalizer@7ef20235 isEnqueued:true
references.PhantomReferenceDemo$MyResourceFinalizer@27d6c5e0 isEnqueued:true
references.PhantomReferenceDemo$MyResourceFinalizer@4f3f5b24 isEnqueued:true
虚引用被回收:references.PhantomReferenceDemo$MyResourceFinalizer@2d98a335
释放内存地址:1265094477
虚引用被回收:references.PhantomReferenceDemo$MyResourceFinalizer@16b98e56
释放内存地址:2125039532
虚引用被回收:references.PhantomReferenceDemo$MyResourceFinalizer@4f3f5b24
释放内存地址:1554874502
虚引用被回收:references.PhantomReferenceDemo$MyResourceFinalizer@7ef20235
释放内存地址:312714112
虚引用被回收:references.PhantomReferenceDemo$MyResourceFinalizer@27d6c5e0
释放内存地址:692404036