深入分析StrongReference,SoftReference, WeakReference和PhantomReference

1、前言

在java中,我们知道一般情况下当一个对象被其他对象引用时,该对象则不会被回收。但是有时我们虽然需要使用该对象,但又希望不影响回收。
比如在Activity中以内部类的方式创建了一个Handler,这个Handler就会隐式的持有一个activity的引用,当这个Handler被一个耗时线程所引用。这时如果关闭这个Activity,由于被引用该Activity及它所持有的引用占用的内存将不能被销毁,这样就导致了内存泄漏。
这时候我们可以使用“弱引用”来解决问题。

2、四种引用

除了之前提到的“弱引用”,在java中还有另外三种引用,下面我们简单谈谈这四种引用:
  • 强引用:就是代码中普遍存在的引用,一般情况下只要存在强引用就不会被回收。(这里要注意相互引用的情况,我们会在另外一篇来说
  • 软引用(SoftReference):只有软引用关联的对象,当内存不足时会被回收(细节下面会说)
  • 弱引用(WeakReference):在广义上除了强引用都是弱引用,这里我们说的是狭义上的弱引用。弱引用比软引用还要更容易被回收,当GC过程中发现只有弱引用的对象时,不论内存是否足够都会被回收。
  • 虚引用(PhantomReference):虚引用对对象的生存不产生任何影响,而且通过虚引用无法获取对象实例。虚引用的作用是我们可以通过它来判断对象是否已经被回收,细节我们下面再聊。
以上就是java中四种引用,至于他们的使用方法都比较简单,大家可以自行搜索文章。

3、java.lang.ref

前面提到的几种引用都在java.lang.ref包下,该包下的类如图:

注意这是Android-26下的对应包,而不是jdk下的包,android系统对jdk的一部分类有一些改动,所以源码有所不同。jdk下该包的类如图:

可以看到jdk下多了 Finalizer和 FinalReference这两个类。其中 FinalReference是 Reference的子类,而 Finalizer则是 FinalReference的子类。

本章我们讨论Android系统下的引用,java引用我们以后另开一章来讨论。
其中 SoftReference、 WeakReference、 PhantomReference都是Reference的子类,而ReferenceQueue则是 Reference的一个重要的组成部分。
下面我们来看看这几个类的源码。

4、Reference

源码如下:
[java]  view plain  copy
  1. public abstract class Reference<T> {  
  2.     private static boolean disableIntrinsic = false;  
  3.     private static boolean slowPathEnabled = false;  
  4.   
  5.     volatile T referent;        /* Treated specially by GC */  
  6.     final ReferenceQueue<? super T> queue;  
  7.   
  8.     Reference queueNext;  
  9.     Reference<?> pendingNext;  
  10.   
  11.     public T get() {  
  12.         return getReferent();  
  13.     }  
  14.   
  15.     @FastNative  
  16.     private final native T getReferent();  
  17.   
  18.     public void clear() {  
  19.         clearReferent();  
  20.     }  
  21.   
  22.     @FastNative  
  23.     native void clearReferent();  
  24.   
  25.     public boolean isEnqueued() {  
  26.         return queue != null && queue.isEnqueued(this);  
  27.     }  
  28.   
  29.     public boolean enqueue() {  
  30.       return queue != null && queue.enqueue(this);  
  31.     }  
  32.   
  33.     Reference(T referent) {  
  34.         this(referent, null);  
  35.     }  
  36.   
  37.     Reference(T referent, ReferenceQueue<? super T> queue) {  
  38.         this.referent = referent;  
  39.         this.queue = queue;  
  40.     }  
  41. }  


代码只有二三十行,我们看到Reference除了带有对象引用referent的构造函数,还有一个带有 ReferenceQueue参数的构造函数。那么这个 ReferenceQueue用来做什么呢?需要我们从enqueue这个函数来开始分析。 当系统要回收Reference持有的对象引用referent的时候,Reference的enqueue函数会被调用,而在这个函数中调用了 ReferenceQueueenqueue函数。那么我们来看看 ReferenceQueue的enqueue函数做了什么?

5、ReferenceQueue.enqueue(Reference)

源码如下:
[java]  view plain  copy
  1. boolean enqueue(Reference<? extends T> reference) {  
  2.     synchronized (lock) {  
  3.         if (enqueueLocked(reference)) {  
  4.             lock.notifyAll();  
  5.             return true;  
  6.         }  
  7.         return false;  
  8.     }  
  9. }  


可以看到首先获取同步锁,然后调用了enqueueLocked( Reference)函数,该函数源码如下:
[java]  view plain  copy
  1. private boolean enqueueLocked(Reference<? extends T> r) {  
  2.     // Verify the reference has not already been enqueued.  
  3.     if (r.queueNext != null) {  
  4.         return false;  
  5.     }  
  6.   
  7.     if (r instanceof Cleaner) {  
  8.         Cleaner cl = (sun.misc.Cleaner) r;  
  9.         cl.clean();  
  10.         r.queueNext = sQueueNextUnenqueued;  
  11.         return true;  
  12.     }  
  13.   
  14.     if (tail == null) {  
  15.         head = r;  
  16.     } else {  
  17.         tail.queueNext = r;  
  18.     }  
  19.     tail = r;  
  20.     tail.queueNext = r;  
  21.     return true;  
  22. }  


通过  enqueueLocked函数可以看到 ReferenceQueue维护了 一个队列(链表结构),而enqueue这一系列函数就是将reference添加到这个队列(链表)中。

6、ReferenceQueue.isEnqueued()

让我们回到Reference源码中,可以看到除了enqueue这个函数还有一个isEnqueued函数,同样这个函数调用了 ReferenceQueue的同名函数,源码如下:
[java]  view plain  copy
  1. boolean isEnqueued(Reference<? extends T> reference) {  
  2.     synchronized (lock) {  
  3.         return reference.queueNext != null && reference.queueNext != sQueueNextUnenqueued;  
  4.     }  
  5. }  


可以看到先获取同步锁,然后判断该reference是否在队列(链表)中。由于enqueue和isEnqueue函数都要申请同步锁,所以这是线程安全的。

这里要注意“ reference.queueNext != sQueueNextUnenqueued”用于判断该Reference是否是一个Cleaner类,在上面 ReferenceQueue的 enqueueLocked函数中我们可以看到如果一个Reference是一个Cleaner,则调用它的clean方法,同时并不加入链表,并且将其queueNext设置为sQueueNextUnequeued,这是一个空的虚引用,如下:
[java]  view plain  copy
  1. private static final Reference sQueueNextUnenqueued = new PhantomReference(nullnull);  


那么什么是Cleaner?引用一段描述
sun.misc.Cleaner是JDK内部提供的用来释放非堆内存资源的API。JVM只会帮我们自动释放堆内存资源,但是它提供了回调机制,通过这个类能方便的释放系统的其他资源。
可以看到Cleaner是用于释放非堆内存的,所以做特殊处理。

通过enqueue和isEnqueue两个函数的分析,ReferenceQueue队列维护了那些被回收对象referent的Reference的引用,这样通过isEnqueue就可以判断对象 referent是否已经被回收,用于一些情况的处理

7、SoftReference

软引用源码如下:
[java]  view plain  copy
  1. public class SoftReference<T> extends Reference<T> {  
  2.   
  3.     static private long clock;  
  4.     private long timestamp;  
  5.   
  6.     public SoftReference(T referent) {  
  7.         super(referent);  
  8.         this.timestamp = clock;  
  9.     }  
  10.   
  11.     public SoftReference(T referent, ReferenceQueue<? super T> q) {  
  12.         super(referent, q);  
  13.         this.timestamp = clock;  
  14.     }  
  15.   
  16.     public T get() {  
  17.         T o = super.get();  
  18.         if (o != null && this.timestamp != clock)  
  19.             this.timestamp = clock;  
  20.         return o;  
  21.     }  
  22.   
  23. }  


可以看到SoftReference有一个类变量clock和一个变量timestamp,这两个参数对于 SoftReference至关重要。
  • clock:记录了上一次GC的时间。这个变量由GC(garbage collector)来改变。
  • timestamp:记录对象被访问(get函数)时最近一次GC的时间。
那么这两个参数有什么用?
我们知道软引用是当内存不足时可以回收的。但是这只是大致情况, 实际上软应用的回收有一个条件:
clock -timestamp <= free_heap * ms_per_mb
  • free_heap是JVM Heap的空闲大小,单位是MB
  • ms_per_mb单位是毫秒,是每MB空闲允许保留软引用的时间。Sun JVM可以通过参数-XX:SoftRefLRUPolicyMSPerMB进行设置
举个栗子:
目前有3MB的空闲, ms_per_mb为1000,这时如果clock和timestamp分别为5000和2000,那么
5000 - 2000 <= 3 * 1000
条件成立,则该次GC不对该软引用进行回收。
所以每次GC时,通过上面的条件去判断软应用是否可以回收并进行回收,即我们通常说的内存不足时被回收。

8、WeakReference

弱引用的源码很简单,如下:
[java]  view plain  copy
  1. public class WeakReference<T> extends Reference<T> {  
  2.   
  3.     public WeakReference(T referent) {  
  4.         super(referent);  
  5.     }  
  6.   
  7.     public WeakReference(T referent, ReferenceQueue<? super T> q) {  
  8.         super(referent, q);  
  9.     }  
  10.   
  11. }  

没有其他代码,GC时被回收掉。

9、PhantomReference

虚引用的源码也比较简单,如下:
[java]  view plain  copy
  1. public class PhantomReference<T> extends Reference<T> {  
  2.   
  3.     public T get() {  
  4.         return null;  
  5.     }  
  6.   
  7.     public PhantomReference(T referent, ReferenceQueue<? super T> q) {  
  8.         super(referent, q);  
  9.     }  
  10.   
  11. }  


可以看到get函数返回null,正如前面说得虚引用无法获取对象引用。(注意网上有些文章说虚引用不持有对象的引用,这是有误的,通过构造函数可以看到虚引用是持有对象引用的,但是无法获取该引用)
同时可以看到虚引用只有一个构造函数,所以必须传入ReferenceQueue对象。
前面提到虚引用的作用是判断对象是否被回收,这个功能正是通过ReferenceQueue实现的(文章第5、6点讲的)。
这里注意:不仅仅是虚引用可以判断回收,弱引用和软引用同样实现了带有ReferenceQueue的构造函数,如果创建时传入了一个ReferenceQueue对象,同样也可以判断。

10、总结

本篇文章主要分析了Reference及其子类的源码,其中 Reference和 ReferenceQueue只分析了部分重点代码,其他代码的作用大家可以自己研究一下。本次的源码分析只涉及到java层,至于底层GC部分并未涉及,以后有机会我们用新的一章来总结。另外对于强引用没有做详细分析,包括相互引用的回收等情况,同样我会找个时间整理一下。谢谢大家!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值