软引用/弱引用/虚引用 源码分析及示例

本篇文章的目的是要总结在Java当中常见的引用与垃圾回收之间的关系,主要介绍的包括引用的概念,引用队列的概念,弱引用,软引用以及虚引用的原理,使用场景。

在进行引用的学习当中,需要区分被引用对象,引用对象之间的区别哦。

Reference基础知识

Reference对象通过泛型存储了对其他对象的引用,之所以需要通过这种方式来创建一个引用对象,是因为可以通过refrence对象建立其存储的对象与垃圾回收之间的关系,能够做到当虚拟机进行内存回收时,不同的reference类型能够影响该对象是何时被系统回收。

reference类当中的几个重要的变量及方法:

    volatile T referent; // 被建立引用的对象
    final ReferenceQueue<? super T> queue; // 建立该引用时关联的队列,在某些类型的引用种,这个可以为null,有些引用类型当中,必须包含引用队列

引用队列ReferenceQueue

为使介绍三种具体引用时更加清晰,这里先对上述提到的引用队列做介绍

  • 既然我们能够通过引用类型建立被引用对象在gc过程中的管理,那么我们如何管理这个引用对象的状态呢?
    • 引用队列便是用来管理引用对象状态的数据结构,他并不是用来存储被引用对象的,因为引用对象自然会由vm对其进行内存回收,而引用队列是当对象被回收后,存储被回收对象引用的,而其具体的管理方式,将在不同的引用类型当中进行介绍。
    • 如果创建引用的时候关联了某个队列,那么便可以通过监听队列的状态来判断该对象是否已经被回收了。

了解以上基础概念之后,便可以介绍继承自reference抽象类的三种类:软引用、弱引用以及虚引用对于其关联的对象的回收状态的管理以及其与引用队列之间的关系。

三种引用类型介绍

软引用(SoftReference)

当一个对象只有软引用的时候,vm只会在系统内存不够的时候对其被引用对象进行回收。

  • 软引用用来描述一些有用但是并非必须的对象
  • 软引用可以配合引用队列使用,当一个软引用引用的对象被系统回收之后,那么引用便会被添加到引用队列当中
  • 软引用常用作创造缓存

虚引用(PhantomReference)

虚引用并不关心每个对象的生命周期,它没有对该对象建立任何引用,该对象随时可以被gc回收,而他唯一的作用就是跟踪该对象是否被回收了

  • 虚引用的构造函数,必须使用有引用队列,这样便可以通过对引用队列的监听,判断你的对象是否被回收了
  • 虚引用的get方法始终返回null
  • 当GC准备回收一个对象的时候,如果发现该对象还有一个虚引用,就会将这个虚引用加入到与之关联的队列,所以其添加进队列的时机是在回收之前的

弱引用(WeakReference)

当一个对象只有弱引用的时候,只要vm扫到这一片区域且并不关心当前内存是否不足,都会被系统进行回收,弱引用的对象较之软引用相比有更脆弱的生存周期,但是相对来说,比虚引用的生命周期强一点

弱引用通常被用于WeakHashMap,以及ThreadLocal当中

WeakHashMap
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }

    @SuppressWarnings("unchecked")
    public K getKey() {
        return (K) WeakHashMap.unmaskNull(get());
    }

}
  • 可以看到,在WeakHashMap当中,他存储的Entry是一个弱引用,并且关联了一个引用队列,因此如果WeakHashMap当中的某一个key只有弱引用的时候,那么这个entry会被放入与这个WeakHashMap所关联的引用队列当中
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }
  • 由如上函数所示,WeakHashMap会访问这个引用队列,读取其中的弱引用的entry,把他从tab当中删除掉
  • 如此,这个map便可以达到自动删除那些没有引用的entry的目的了,对于普通的hashMap则不会有这个特性,就算是key不再具有引用了,他也不会自动从map当中被删除
ThreadLocal

ThreadLocal提供的对与对象的管理,主要用于进行同一个对象在不同线程之间的数据管理,通过ThreadLocal管理的对象,会在每一个线程当中提供该对象的副本,而使得不同线程对于该对象的访问和更改完全隔离,我们知道如果不使用ThreadLocal,那么所有线程对于变量的访问,均是使用同一个对象,而如果不进行线程同步,则会使访问修改产生误差

ThreadLocal的使用则需要进行初始化,利用get和set进行访问

  • 原理介绍

    • ThreadLocal管理的是每一个线程对同一个对象副本的访问,因为管理的是同一个对象,被ThreadLocal修饰后,其值是交给每一个线程进行自己的数据管理,而这个管理的过程又是交由threadLocalMap进行管理的

    • public void set(T value) {
          Thread t = Thread.currentThread();
          ThreadLocalMap map = getMap(t);
          if (map != null)
              map.set(this, value);
          else
              createMap(t, value);
      }
      
      ThreadLocalMap getMap(Thread t) {
          return t.threadLocals;
      }
      
    • 可以看到,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

    • 而这个访问的过程则是通过利用map进行访问的,每一个线程的threadLocals都是一个ThreadLocalMap,这个map的key值为当前的ThreadLocal变量,值为value,进行访问的时候也只会访问自己的key与value,因此便实现了线程之间的数据隔离

    • static class Entry extends WeakReference<ThreadLocal<?>> {
          /** The value associated with this ThreadLocal. */
          Object value;
      
          Entry(ThreadLocal<?> k, Object v) {
              super(k);
              value = v;
          }
      }
      
    • 可以看到,在ThreadLocalMap当中的Entry仍然是一个弱引用,与WeakHashMap相同,其中的key是一个ThreadLocal变量,这样,当一个线程在线程池当中需要复用的时候,而其存储的threadLocals当中的key即这个ThreadLocal没有外部的强引用的时候,便可以被系统回收;而与WeakHashMap类似,ThreadLocalMap也存在对于线程当中已经回收的key的自动释放value逻辑,由于其没有在创建的时候关联引用队列,那么只能遍历table来释放掉value的内存啦

项目当中使用弱引用的例子

在我们项目当中有一个用于通知外部ListenerMgr,其主要作用便是进行通过注册所有的回调对象,完成对所有注册过的场景进行通知的作用,这个通知的场景,常用于发生在注册的数据model请求数据回来后,通知拿到了后台数据进行下一步操作

之前在进行组件化的过程中,为了进行跨module的数据通知,便想通过引用这个管理回调,但是由于不知道弱引用的原理,导致产生了bug,接下来利用以上弱引用的知识进行复盘

ListenerMgr源码

public class ListenerMgr<T> {
    private final ConcurrentLinkedQueue<WeakReference<T>> mListenerQueue = new ConcurrentLinkedQueue();

    public ListenerMgr() {
    }

    public void register(T listener) {
        if (listener != null) {
            synchronized(this.mListenerQueue) {
                boolean contain = false;
                Iterator iterator = this.mListenerQueue.iterator();

                while(iterator.hasNext()) {
                    T listenerItem = ((WeakReference)iterator.next()).get();
                    if (listenerItem == null) {
                        iterator.remove();
                    } else if (listenerItem == listener) {
                        contain = true;
                    }
                }

                if (!contain) {
                    WeakReference<T> weakListener = new WeakReference(listener);
                    this.mListenerQueue.add(weakListener);
                }

            }
        }
    }
    public void startNotify(ListenerMgr.INotifyCallback<T> callback) {
        、、、
      // 读取操作
                while(true) {
                    Object listenerItem;
                    do {
                        if (!var3.hasNext()) {
                            return;
                        }

                        WeakReference<T> aCopyListenerQueue = (WeakReference)var3.next();
                        listenerItem = aCopyListenerQueue.get();
                    } while(listenerItem == null);

                    try {
                        callback.onNotify(listenerItem);
                    } catch (final Throwable var8) {
                        var8.printStackTrace();
                        Log.e("crash", var8.toString(), var8);
                        if (isDebug) {
                            Handler handler = new Handler(Looper.getMainLooper());
                            handler.post(new Runnable() {
                                public void run() {
                                    throw new RuntimeException(var8);
                                }
                            });
                        }
                    }
                }
            } catch (Throwable var9) {
            }
        }

    }
  • 可以看到,ListenerMgr当中存储了一个队列,这个队列当中存储的是所有需要注册该回调的弱引用对象,其register过程是判断队列当中是否有该回调对象存在,如果不存在就创建一个弱引用,然后加入队列
  • 通知的过程则是通过传入的callback对象,遍历到队列当中当前弱引用对象不为空是,便调用传入的这个callback的onNotify方法,来执行每一个listenerItem的回调函数
跨组件的调用导致的问题
  • /**
    *  CommonFragmentConfig,负责注册监听,完成通知的调用
    **/
    public class CommonFragmentConfig {
        private static final ListenerMgr<CommonFragment.ICommonFragmentVisibilityObserver>
                OBSERVER_LISTENER_MGR = new ListenerMgr<>();
    
      /**
      *  registerCommonFragmentVisible 传入外部访问注册的回调监听对象
      *  observer 外部监听对象
      **/
        public static void registerCommonFragmentVisible(CommonFragment.ICommonFragmentVisibilityObserver observer) {
            OBSERVER_LISTENER_MGR.register(observer);
        }
    
        public static void notifyOnFragmentInvisible(CommonFragment fragment) {
            OBSERVER_LISTENER_MGR.startNotify(commonFragmentVisibilityObserver ->
                    commonFragmentVisibilityObserver.onFragmentInvisible(fragment));
        }
    
        public static void notifyOnFragmentVisible(CommonFragment fragment) {
            OBSERVER_LISTENER_MGR.startNotify(commonFragmentVisibilityObserver ->
                    commonFragmentVisibilityObserver.onFragmentVisible(fragment));
        }
    }
    
    public class CommonFragmentVisibleConfig {
        public static void init() {
            CommonFragmentConfig.registerCommonFragmentVisible(new CommonFragment.ICommonFragmentVisibilityObserver() {
                @Override
                public void onFragmentInvisible(CommonFragment commonFragment) {
                    FloatLayerManager.sharedInstance().onFragmentInVisible(commonFragment);
                }
    
                @Override
                public void onFragmentVisible(CommonFragment commonFragment) {
                    FloatLayerManager.sharedInstance().onFragmentVisible(commonFragment);
                }
            });
        }
    }
    
    • 可以看到,这里在CommonFragmentVisibleConfig注册过程当中,传入的是一个new出来的对象,那么这个对象自然没有其他的引用存在,通过调用CommonFragmentConfig的registerCommonFragmentVisible方法,自然只能将这个对象放入弱引用队列mListenerQueue当中
    • 由于跨组件初始化的过程即CommonFragmentVisibleConfig的init()方法的调用是在app启动的时候,这时候的gc比较频繁,自然这个new出来的回调对象就会被回收掉了,也不会再有后续的通知CommonFragment可见性变化的方法啦
  • 数据请求model的使用情况

    • public class VideoPublishPageController extends implements IModelListener {
      
         
          private GetPublishEntryModel mGetPublishEntryModel;
      
          private boolean mVideoLoadFinish = false;
      
          public VideoPublishPageController(Fragment fragment, View rootView, EventBus eventBus,
                                            @NonNull AdapterContext adapterContext, PublishVideoData data) {
              super(fragment, rootView, eventBus, adapterContext, data);
              init();
              loadEntriesData();
          }
      
          private void loadEntriesData() {
              if (mGetPublishEntryModel == null) {
                  mGetPublishEntryModel = new GetPublishEntryModel();
                  mGetPublishEntryModel.register(this);
              }
              mGetPublishEntryModel.loadData();
          }
        
            @Override
          public void onLoadFinish(AbstractModel model, int errCode, boolean isCache,
                                   Object info) {
               if (model instanceof GetPublishEntryModel) {
                  onGetPublishEntryModelLoadFinish(errCode, info);
              }
          }
      }
      
    • 可以看到其实这里mGetPublishEntryModel.register(this);也是同样的利用了ListenerMgr进行了数据的监听,那么自然也是放入了ListenerMgr当中的队列当中,管理了一个弱引用的对象

    • 但是由于这个回调对象是VideoPublishPageController这个,即mListenerQueue当中的弱引用对象当中的referent是VideoPublishPageController的实例,而VideoPublishPageController的实例被外部的fragment所持有,所以并不是一个没有外部引用的弱引用对象,那么就自然不会被gc回收掉,而只要这个VideoPublishPageController对象存活,就能持续的请求的回调,执行onLoadFinish()方法啦

补充

  • 其实弱引用还用于检测app的内存泄漏,比较有名的LeakCanary便是利用了这个原理,后续可能会对其源码进行原理分析及补充
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容