文章目录
- 1. 基本概念
- 2. 代码演示
- 2.1 软引用代码演示
- 2.2 弱引用代码演示
- 2.3 弱引用+引用队列代码演示
- 2.4 虚引用代码演示
- 2.5 虚引用+引用队列代码演示
- 3. 实战样例
- 3.1 利用软引用实现资源对象缓存
- 3.2 利用弱引用实现临时行为数据缓存
- 3.3 利用虚引用+引用队列实现资源释放
本次实验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的强引用,然后执行GCobj1 = null;System.gc();// 解除obj2的强引用,不执行GCobj2 = 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强引用,然后执行GCobj1 = 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 spaceat 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转为了pdfSystem.out.println(wordToPDF("word1"));// 用户A因为手快连点了两次,由于没有进行缓存,因此又执行了一遍wordToPDFSystem.out.println(wordToPDF("word1"));}System.out.println();// 程序B,使用WeakReference做缓存{System.out.println("---程序B(使用缓存)----");// 用户A将word1转为了pdfSystem.out.println(wordToPDFWithCache("word1"));// 用户A因为手快连点了两次,由于有缓存,直接返回System.out.println(wordToPDFWithCache("word1"));// 过了若干时间System.gc();Thread.sleep(1000);// 用户A再次对word1进行转pdfSystem.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@Overridepublic void run() {while (true) {Thread.sleep(100);// 尝试从引用队列中获取引用对象PhantomReference phantomReference = (PhantomReference) referenceQueue.poll();if (phantomReference == null) {continue;}String resourceId = referenceIdMap.get(phantomReference); // 获取resourceIDResource.realise(resourceId);referenceIdMap.remove(phantomReference);}}}).start();Resource resource1 = Resource.getInstance("res1");Resource resource2 = Resource.getInstance("res2");Resource resource3 = Resource.getInstance("res3");resource3 = null; // resource3使用完毕,执行了GCSystem.gc();Thread.sleep(1000);resource1 = null; // resource1使用完毕,没有执行GCresource2 = null; // resource2使用完毕,执行GCSystem.gc();}
}
输出:
释放资源:res3
释放资源:res2
释放资源:res1