Java中有四种引用类型,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(WeakReference)、虚引用(PhantomReference)。
为什么要将引用分成这四种类型?
这要从Java管理内存的方式说起。Java为了将程序员从内存管理中解救出来,即不让程序员自己申请堆内存(比如C语言程序员需要通过malloc请求操作系统分配一块堆内存给自己使用),自己去释放堆内存(比如C语言程序员需要通过free来释放内存),降低程序员的心智负担,且可以增加开发效率。Java虚拟机通过追踪Java中的对象是否有来自GC Roots的强引用指向它来决定对象所占用的堆内存是否可以回收。
强引用
我们平常写的代码,比如:
String abc = new String(); ListintList = new ArrayList<>();
其中的引用abc和intList都是强引用,也就是说引用默认都是强引用。
软引用
软引用是什么样的呢?如果我们想用软引用,需要通过java.lang.ref.SoftReference,比如:
SoftReferencesoftRef = 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 SoftReferencesoftReference = 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 SoftReferencesoftReference = 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) { WeakReferenceweakReference = 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,那么永远也无法获取到虚引用指向的对象了 PhantomReferencephantomReference = 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{ ReferenceQueuequeue = new ReferenceQueue<>(); // 虚引用需要和引用队列一起使用,这样再垃圾回收完虚引用的对象后,它的虚引用会被放到队列中 PhantomReference phantomReferenceWithQueue = new PhantomReference<>(new MyClass(), queue); // 启动另一个线程来检查是否有虚引用被回收了 new Thread(()->{ while (true){ Reference extends MyClass> 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