Java中的强引用、软引用、弱引用、虚引用与引用队列 通俗举例实战详解
文章目录
本次实验Java版本:JDK 1.8.0_152_release
1. 基本概念
本节先了解一下概念,看不懂没关系,后续会详解。
Java中的引用可以按强弱程度分为四种,JVM对不同程度的引用回收策略不同:
强引用(Strong Reference):我们平时用的都是强引用。例如:MyObject myObj = new MyObject();
- 回收:只要有引用,就不会被回收。
软引用(Soft Reference):使用SoftReference
显式声明。
- 回收:当JVM内存不够时,会对软引用对象进行回收。
- 应用场景:做资源类缓存。
- 使用样例:
MyObject myObject = new MyObject("Amy"); // 从数据库中获取数据 SoftReference<MyObject> reference = new SoftReference<>(myObject); // 增添软引用 // do something ... myObject = reference.get(); // 尝试获取myObject对象 if (myObject == null) { // 没获取到,说明已经被JVM回收了 myObject = new MyObject("Amy"); // 重新从数据库中获取数据 } else { // 没有被JVM回收 }
弱引用(Weak Reference):使用WeakReference
显式声明。
- 回收:当JVM下次执行垃圾回收时,就会立刻回收。
- 应用场景:做临时行为类数据缓存。
- 使用样例:和上面
SoftReference
一样,把SoftReference
改成WeakReference
即可。
虚引用(Phantom Reference):也称为“幽灵引用”、“幻影引用”等。单纯的将其标记一下,配合引用队列(ReferenceQueue
)进行回收前的一些操作
- 回收:当JVM执行垃圾回收时,就会立刻回收
- 应用场景:资源释放(用来代替
finalize
方法) - 特殊点:虚引用的
reference.get()
方法一定会返回null
(源码就是直接return null
,这也是为什么叫虚引用的原因。 - 使用样例:建后续引用队列。
引用队列:在定义软/弱/虚引用时,可以传个引用队列(虚引用必须传),这样对象在被回收之前会进入引用队列,可以显式的对其进行一些操作。
- 作用:可以在该对象被回收时主动收到通知,来对其进行一些操作。
- 应用场景:资源释放
2. 代码演示
本节是对软/弱/虚引用和引用队列的作用机制代码演示。
2.1 软引用代码演示
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
public class SoftReferenceTest {
public static void main(String[] args) {
// 这里定义了obj对象,为正常的强引用
String obj = new String("myObj");
// 为该对象增加软引用
// 此时它同时拥有两种引用,即obj同时拥有强引用和软引用
Reference<String> softReference = new SoftReference<>(obj);
// 解除obj的强引用,此时obj只剩下了软引用
obj = null;
// 调用GC进行垃圾回收。(只是通知,并不一定真的做垃圾回收,这里假设做了)
System.gc();
// 尝试从软引用中获取obj对象
// 若没有获取到,说明该对象已经被JVM释放
// 若获取到了,说明还没有被释放
String tryObj = softReference.get();
System.out.println("try obj:" + tryObj);
}
}
输出结果:
try obj:myObj
解释:
上述样例中,我们为obj
对象绑定了软引用,在解除了强引用后尝试进行GC。可以看到,GC并没有回收obj
对象。这是因为软引用的回收规则为:当JVM内存不够时,才会进行回收,上面的简单例子中,JVM内存显然不会不够用,因此我们可以通过reference
对象拿到obj
对象。
2.2 弱引用代码演示
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
public class WeakReferenceTest {
public static void main(String[] args) {
// 这里定义了obj和obj2对象,为正常的强引用
String obj1 = new String("myObj1");
String obj2 = new String("myObj2");
// 为它们增加弱引用
// 此时它们同时拥有两种引用,即obj同时拥有强引用和弱引用
Reference<String> reference1 = new WeakReference<>(obj1);
Reference<String> reference2 = new WeakReference<>(obj2);
// 解除obj1的强引用,然后执行GC
obj1 = null;
System.gc();
// 解除obj2的强引用,不执行GC
obj2 = null;
/**
* 尝试从若引用中获取obj1对象
* 若没有获取到,说明该对象已经被JVM释放
* 若获取到了,说明还没有被释放
*/
String tryObj1 = reference1.get();
// 这里tryObj1将会返回null,因为obj解除强引用后执行了GC操作
// 因此obj1被释放了,所以获取不到
System.out.println("try obj1:" + tryObj1);
String tryObj2 = reference2.get();
// 这里obj2不是null,因为obj2解除了强引用后并没有执行GC,
// 因此obj2并没有被释放,所以可以获取到
System.out.println("try obj2:" + tryObj2);
}
}
输出结果:
try obj1:null
try obj2:myObj2
解释:弱引用的释放规则为“下次执行GC时,会将对象进行释放”。在上述代码中,obj1
执行了GC,而obj2
没有执行GC,因此obj1
为null
,而obj2
不为null
2.3 弱引用+引用队列代码演示
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
public class WeakReference2Test {
public static void main(String[] args) throws InterruptedException {
// 定义引用队列
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
// 这里定义了obj为正常的强引用
String obj1 = new String("myObj1");
// 为它增加弱引用
// 此时它同时拥有两种引用,即obj同时拥有强引用和弱引用
// 在为obj对象绑定弱引用时,同时传入了引用队列。
// 这样“obj对象”在被回收之前,其对应的“引用对象”就会进入引用队列
Reference<String> reference1 = new WeakReference<>(obj1, referenceQueue); // 这里传入引用队列
System.out.println("reference1: " + reference1);
// 解除obj1强引用,然后执行GC
obj1 = null;
// 尝试从引用队列中获取引用对象。
Reference<String> pollRef1 = (Reference<String>) referenceQueue.poll();
// 这里会返回null,因为obj1虽然没有了强引用,但是还没有被标记为“可回收”
// 因此,“obj1”的引用对象并没有进入到引用队列中
System.out.println("pollRef1: " + pollRef1);
// 执行GC操作
System.gc();
// 给些时间让reference对象进入引用队列
Thread.sleep(100);
// 再次尝试从引用队列中获取“引用对象”
Reference<String> pollRef2 = (Reference<String>) referenceQueue.poll();
// 这次就不为null了,因为`obj1`已经被标记为“可回收”,
// 因此其对应的“引用对象”就会进入引用队列
System.out.println("pollRef2: " + pollRef2);
// 尝试从引用对象中获取`obj`对象
// 这里两种可能:①null;② `myObj1`
// 因为当`obj1`被标记为“可回收”时,其引用对象就会进入引用队列,但此时obj1是否被回收并不知道
// ① 若obj1已经被回收了,这里就会返回null。(也是最常见的)
// ② 若Obj1暂时还没被回收,这里就会返回“myObj1”(这个很难复现)
System.out.println("pollRef2 obj: " + pollRef2.get());
}
}
输出
reference1: java.lang.ref.WeakReference@49c2faae
pollRef1: null
pollRef2: java.lang.ref.WeakReference@49c2faae
pollRef2 obj: null
上述代码中一共进行了两次从引用队列中获取“引用对象”。第一次,由于还没有执行GC操作,obj1
没有被标记为“可回收”状态,所以其对应的引用对象reference1
也就没有进入引用队列。第二次,由于执行了GC操作,obj1
被标记为了“可回收”,因此可以拿到reference1
对象,但由于obj1
已经被回收了,因此使用reference1.get()
却拿不到obj1
对象了。
这里再额外说明几点大家可能疑惑的点:
- 将“①对象标记为是否可回收”和“②释放对象”两个动作基本上是同时发生的,执行完动作①就会执行动作②。所以一般我们都只能从引用队列中拿到“引用对象”,但是再用引用对象去拿对象就拿不到了,因为已经被释放了。
- 只要对象被标记为“可释放”,那么该对象的“引用对象”就会进入引用队列,后续该对象随时可能会被释放。因此有可能会出现以下情况:
Reference<String> pollRef1 = (Reference<String>) referenceQueue.poll(); // 不为null,因为这个时候还没被释放 System.out.println("pollRef1: " + pollRef1.get()); // 为null,被释放了 System.out.println("pollRef1: " + pollRef1.get());
2.4 虚引用代码演示
虚引用必须要配合引用队列,要不然没有任何意义:
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceTest {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 这里定义了obj对象,为正常的强引用
String obj = new String("myObj");
// 为该对象增加虚引用
// 此时它同时拥有两种引用,即obj同时拥有强引用和虚引用
// 虚引用构造方法要求必须要传引用队列
Reference<String> phantomReference = new PhantomReference<>(obj, queue);
// obj的强引用还在,因此其一定不会被释放
// 尝试使用虚引用获取obj对象
String tryObj = phantomReference.get();
// tryObj为null,因为phantomReference.get()的实现就是`return null;`
System.out.println("try obj:" + tryObj);
}
}
输出:
try obj:null
上面的代码中,虽然obj
没有被释放,但用虚引用依然无法获取obj
对象。这也是为什么人们说“虚引用是虚幻的,必须要配合引用队列一起用,要不然就没意义了”
2.5 虚引用+引用队列代码演示
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceTest2 {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<>();
// 这里定义了obj对象,为正常的强引用
String obj = new String("myObj");
// 为该对象增加虚引用
// 此时它同时拥有两种引用,即obj同时拥有强引用和虚引用
// 虚引用构造方法要求必须要传引用队列
Reference<String> phantomReference = new PhantomReference<>(obj, queue);
System.out.println("phantomReference:" + phantomReference);
// 解除obj的强引用
obj = null;
System.gc();
// 从引用队列中获取引用对象。
// 由于obj已经是“可释放或已释放”状态,因此可以获取到`phantomReference`
System.out.println("referenceQueue:" + queue.poll());
}
}
3. 实战样例
3.1 利用软引用实现资源对象缓存
场景:假设我们有一个资源类Resource
,它占用100M内存。加载过程很慢,我们既不想因为导致OOM,又想让它缓存在JVM里,我们就可以用软引用实现。
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
public class SoftReferenceAppTest {
static class Resource {
private byte[] data = new byte[1024 * 1024 * 100]; // 该资源类占用100M内存
private static int num = 0;
private static SoftReference<Resource> resourceHolder = null;
public static Resource getInstance() {
if (resourceHolder != null) {
// 从缓存中获取
Resource resource = resourceHolder.get();
if (resource != null) {
return resource;
}
}
// 缓存中没有,重新new一个
System.out.println("从数据库中获取资源类:" + num);
num ++;
Resource resource = new Resource();
resourceHolder = new SoftReference<>(resource);
return resource;
}
public String getData() {
return "data";
}
}
public static void main(String[] args) {
// 使用资源类
System.out.println(Resource.getInstance().getData());
// 执行各种各样的事情,最终jvm内存满了,然后回收了resource。
int i = 0;
List<byte[]> list = new ArrayList<>();
while(true) {
list.add(new byte[1024*1024*50]);
i++;
// 使用资源类
Resource.getInstance().getData();
if (i > 10000) {
break;
}
}
}
}
输出:
从数据库中获取资源类:0
data
从数据库中获取资源类:1
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at jvm.SoftReferenceAppTest$Resource.<init>(SoftReferenceAppTest.java:11)
at jvm.SoftReferenceAppTest$Resource.getInstance(SoftReferenceAppTest.java:30)
at jvm.SoftReferenceAppTest.main(SoftReferenceAppTest.java:52)
上述例子中,我们的Resource
类使用了软引用SoftReference
来进行缓存。
之后在模拟中,执行过程如下:
- 首先获取了资源类对象,并将这个对象绑定了软引用,但并不存在强引用
- 使用资源类,正常从软引用中获取缓存的资源类对象
- 不断的往JVM中添加数据
list.add(new byte[1024*1024*50]);
,最终导致内存不够用 - 由于内存不够用,因此GC回收时回收了资源类对象,因此重新生成了资源类对象
- 最后,JVM内存不够了,最终OOM
3.2 利用弱引用实现临时行为数据缓存
抽象场景:在某些场景中,用户的操作会产生一些后台数据,而这些数据用户可能会在短时间内再次使用,但也可能不使用。这种时候就可以使用弱引用WeakReference
进行缓存。
样例场景:后台提供了一个word转PDF的功能,但由于某些用户可能会连点,或由于网络不好导致短时间内再次对同一文档进行转换。而转换过程又比较耗资源,因此使用WeakReference
进行缓存。
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class WeakReferenceAppTest {
// 缓存对象
static Map<String, WeakReference<String>> cacheMap = new HashMap<>();
// 将word数据转为pdf数据,不使用缓存
public static String wordToPDF(String wordData) {
return "pdf " + wordData;
}
// 将word数据转为pdf数据,使用缓存
public static String wordToPDFWithCache(String wordData) {
// 先看缓存里有没有
if (cacheMap.keySet().contains(wordData)) {
String pdfData = cacheMap.get(wordData).get();
if (pdfData != null) {
System.out.println("使用缓存数据");
return pdfData;
}
}
String pdfData = "pdf " + wordData;
System.out.println("无缓存,执行转换!");
cacheMap.put(wordData, new WeakReference<>(pdfData));
return pdfData;
}
public static void main(String[] args) throws InterruptedException {
// 程序A,不使用WeakReference做缓存
{
System.out.println("---程序A(不用缓存)----");
// 用户A将word1转为了pdf
System.out.println(wordToPDF("word1"));
// 用户A因为手快连点了两次,由于没有进行缓存,因此又执行了一遍wordToPDF
System.out.println(wordToPDF("word1"));
}
System.out.println();
// 程序B,使用WeakReference做缓存
{
System.out.println("---程序B(使用缓存)----");
// 用户A将word1转为了pdf
System.out.println(wordToPDFWithCache("word1"));
// 用户A因为手快连点了两次,由于有缓存,直接返回
System.out.println(wordToPDFWithCache("word1"));
// 过了若干时间
System.gc();
Thread.sleep(1000);
// 用户A再次对word1进行转pdf
System.out.println(wordToPDFWithCache("word1"));
}
}
}
输出:
---程序A(不用缓存)----
pdf word1
pdf word1
---程序B(使用缓存)----
无缓存,执行转换!
pdf word1
使用缓存数据
pdf word1
无缓存,执行转换!
pdf word1
3.3 利用虚引用+引用队列实现资源释放
应用场景:替代finalize,因为其有很多缺点:
finalize的缺点:
(1)无法保证什么时间执行。
(2)无法保证执行该方法的线程优先级。
(3)无法保证一定会执行。
(4)如果在终结方法中抛出了异常,并且该异常未捕获处理,则当前对象的终结过程会终止,且该对象处于破坏状态。
(5)影响GC的效率,特别是在finalize方法中执行耗时较长的逻辑。
(6)有安全问题,可以进行终结方法攻击。
最优解决方案:使用try-with-resource。
其次解决方案:使用虚引用+引用队列。
最好两种同时用。
样例场景:我们现在有一个比较耗资源的Resource类(也可以是IO等),我们没有办法保证try-with-resource
和finalize
能正确释放。因此我们还要启一个后台线程来监控目前是否有未释放的资源。如果有,则释放资源。若释放失败,则发送通知消息。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
public class PhantomReferenceAppTest {
// 引用队列,专门负责对Resource对象的释放
static ReferenceQueue<Resource> referenceQueue = new ReferenceQueue<>();
static Map<PhantomReference, String /*资源ID*/> referenceIdMap = new HashMap<>();
static class Resource {
public static Resource getInstance(String resourceID) {
Resource resource = new Resource();
// 将resource与一个虚引用绑定,并传入公共引用队列
// 这样当resource资源不再使用时,就可以释放资源。
PhantomReference phantomReference = new PhantomReference<>(resource, referenceQueue);
referenceIdMap.put(phantomReference, resourceID); // 记录这个引用对象对应的资源ID,用于后续释放资源
return resource;
}
// 注意:这个必须是静态方法。因为PhantomReference根本拿不到Resource的实例。
// 假设使用软/弱引用,也不一定能拿到Resource,因为Resource对象很有可能已经被JVM释放了
public static void realise(String resourceID) {
System.out.println("释放资源:" + resourceID);
}
}
public static void main(String[] args) throws InterruptedException {
// 定义一个专门用于资源释放的线程
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
while (true) {
Thread.sleep(100);
// 尝试从引用队列中获取引用对象
PhantomReference phantomReference = (PhantomReference) referenceQueue.poll();
if (phantomReference == null) {
continue;
}
String resourceId = referenceIdMap.get(phantomReference); // 获取resourceID
Resource.realise(resourceId);
referenceIdMap.remove(phantomReference);
}
}
}).start();
Resource resource1 = Resource.getInstance("res1");
Resource resource2 = Resource.getInstance("res2");
Resource resource3 = Resource.getInstance("res3");
resource3 = null; // resource3使用完毕,执行了GC
System.gc();
Thread.sleep(1000);
resource1 = null; // resource1使用完毕,没有执行GC
resource2 = null; // resource2使用完毕,执行GC
System.gc();
}
}
输出:
释放资源:res3
释放资源:res2
释放资源:res1