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