前言
文章
- 相关系列:《Java ~ Reference【目录】》(持续更新)
- 相关系列:《Java ~ Reference ~ WeakReference【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Reference ~ WeakReference【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Reference ~ WeakReference【问题】》(学习解答/持续更新)
- 涉及内容:《Java ~ Collection ~ WeakHashMap【总结】》
- 涉及内容:《Java ~ Thread ~ ThreadLocalMap【总结】》
- 涉及内容:《Java ~ Collection/Executor ~ ArrayBlockingQueue【总结】》
一 概述
简介
WeakReference(弱引用)类是Reference(引用)抽象类的四大子类之一,只被弱引用持有的对象被称为弱可达(weakly reachable)对象。在Java中关于弱引用的定义是:弱引用等价于无引用,其存在无法对对象的生命周期产生任何影响,即对象的诞生、初始化、使用及回收完全与弱引用无关。这不禁会令人产生疑惑…如果弱引用就是无引用,那引入所谓的弱引用又有什么意义呢?这不是脱裤子放屁多次一举吗?
事实上有这样的疑惑是非常正常的,因为基本上所有的资料都会在有意或无意中将弱引用和无引用画上等号。如此叙述其实并没有错,但覆盖面过于广泛,从而令学习者无法真正明晰两者的区别。想要真正明白两者差异需要结合更高等级的引用加以对比,以强引用举例:一个对象拥有一个强引用以及九个弱引用和一个对象拥有十个强引用有区别吗?有!而且非常大!前者只需断开一个强引用即可GC回收,而后者却需要断开所有的十个强引用才可以。故而可知,弱引用与无引用的等价性仅限于GC方面,即两者都不会成为GC回收对象的影响因素;但访问方面,软引用则与强引用等价,即两者都可以成为对象的访问通道。
弱引用类常用于帮助GC回收。很多资料都会说弱引用适合用作缓存,可但凡思考过其特性的话应该都不会这么想…一个需要配合强引用才能够保证自身存在的对象适合做哪门子缓存?这不就是个常规的强引用对象么?真正适合做缓存的应该是软引用才对。基于可有可无的引用强度,即使是在没有共存强引用的环境下,只要空闲内存充裕,软引用自身也能够保证热点数据持久存在,这与缓存的运用场景是高度契合的。弱引用的作用主要是用于帮助GC回收,就像上文中举例的场景:一个拥有一个强引用以及九个弱引用的对象和一个拥有十个强引用的对象哪个更容易被GC回收?显然是前者,因为其只需要断开一个强引用就可以了。这个特性在实际开发中非常有用,不仅可以处理遗漏/难以/无法断开一些比较隐秘的强引用而使得对象无法被GC回收最终导致OOM的情况,也有助于简化GC回收。
二 使用
创建
-
public WeakReference(T referent) —— 创建指定所指对象但未注册引用队列的弱引用。
-
public WeakReference(T referent, ReferenceQueue<? super T> q) —— 创建指定所指对象及注册引用队列的弱引用。
方法
弱引用类没有定义新方法,其所有方法皆继承/实现自引用抽象类。
-
public T get() —— 获取 —— 获取当前弱引用的所指对象,当所指对象不存在时返回null。所指对象初始是必然存在的,但可以在后期被清除。
-
public void clear() —— 清除 —— 清除当前弱引用的所指对象(即断开两者的引用关系),并不会将当前弱引用加入到注册引用队列中。该方法专为开发者提供,GC线程不会调用该方法断开当前弱引用与其所指对象的关联。
-
public boolean isEnqueued() —— 是否入队 —— 判断当前弱引用是否已加入注册引用队列,是则返回true;否则返回false。引用加入注册引用队列时会将自身注册的引用队列替换为“入队”引用队列,这是一个在引用队列类内部创建的全局静态引用队列,被作为引用加入注册引用队列的标志位来使用。因此判断当前引用是否加入注册引用队列无需遍历注册引用队列,直接判断注册引用队列是否是“入队”引用队列即可。
-
public boolean enqueue() —— 入队 —— 将当前弱引用加入注册引用队列中,成功返回true;否则返回false。该方法底层调用引用队列类的enqueue(Reference<? extends T> r) 方法实现。该方法专为开发者提供,“引用处理器”线程不会调用该方法将当前弱引用加入注册引用队列。
三 使用案例
WeakHashMap(弱哈希映射)类
弱哈希映射类与HashMap(哈希映射)类同为Map(映射)类的主流实现之一,且两者作用相似。与哈希映射类不同的是,弱哈希映射类中用于实现键值对结构的Entry(条目)类直接继承于弱引用类,因此该类的对象本身也是弱引用。条目类会将自身的所指对象作为键来使用,这就使得一旦所指/键对象断开了弱哈希映射类外部的强引用就会被GC回收,并将其对应的弱引用,也就是条目加入到引用队列中。
弱哈希映射类如此设计直接目的是为了实现半自动化的过期条目清理机制,而核心目的则是为了节省内存,更准确的说是在一定程度上减少内存泄露的情况。在某些情况下条目只会短期使用,但映射却往往是长时间存在的,这就使得映射在长时间运行后可能存有大量过期(即不会再使用)条目占用内存,即所谓的内存泄露。在条目对键对象是强引用的情况下,为了消除内存泄露我们必须在每个键对象断开外部强引用时手动地调用方法将相关条目从映射中移除。这么做并非不可行但操作相对复杂,并且在实际开发中开发者往往会以整个映射为单位进行操作,这是经常使用哈希映射类而养成的编程习惯,因此如此行为并不符合主流的使用方式。而这一切在条目类继承了弱引用类后就变的简单了许多。首先,条目与所指/键对象之间由强引用变为了弱引用关系,因此开发者无需再关注每个键对象断开强外部引用的时机,也无需再手动调用方法移除相关条目。键对象的回收会在其断开外部强引用时自动发生,并且在搭配引用队列的情况下,其所属条目也会被GC自动加入到引用队列中。由此,当开发者后续调用弱哈希映射的方法时就可以触发对引用队列中过期条目的清理,从而消除内存泄露的情况,这是一种半自动化的清理方式。
注意:弱哈希映射类的清理并无法保证完全消除内存泄露的情况,具体原因有二:一是清理是半自动化的,需要调用弱哈希映射类的方法进行触发,因此在不进行后续调用的情况下并无法清理其中的过期条目(但一般来说如果不进行后续的调用那大概率是整个弱哈希映射都要被放弃了);二是虽然弱哈希引用类是线程不安全的实现类,但也可能被用于多线程的环境中,因此在并发的情况下即使清理被触发也无法保证完全清理。
/**
* Reference queue for cleared WeakEntries
* 引用队列,用于存放键对象已经被GC回收的Entry类对象,以便将之用节点数组中删除。
*/
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
private static class Entry<K, V> extends WeakReference<Object> implements Map.Entry<K, V> {
V value;
final int hash;
Entry<K, V> next;
...
}
ThreadLocalMap(线程本地映射)类
线程本地映射类是ThreadLocal(线程本地)类中自实现的内部静态映射类,需要注意的是其不是Map(映射)类的子类。线程本地映射类与弱哈希映射类一样都条目类直接继承于弱引用类,并且在所指/键对象的GC回收上也是相同的机制。但线程本地映射类没有搭配引用队列使用,因此过期条目并不会被加入到引用队列中。但由于线程本地映射类在寻址操作需要频繁的遍历条目数组,因此这些过期条目会在遍历的过程中被清理。
线程本地映射类如此设计的原因与弱哈希映射类一致,并额外辅助GC对线程本地的回收。众所周知线程本地映射会以线程本地为键对象,而由于线程本地映射存在于线程中,这就意味着一个线程本地可能同时被成千上万个线程本地映射的条目持有引用。如果这些引用都是强引用的话,那仅仅在全局层面上断开线程本地的强引用显然是不够的,还需要每个线程全部断开才能使线程本地正常的被GC回收。但如果设计为弱引用的话,那一但全局的强引用被断开,那其余所有的弱引用都会等价于无引用(GC方面),故而线程本地自然也就能够顺利的GC回收了。
注意:与弱哈希引用类形同,基于半自动化及并发的原因半自动化的,无论清理是否被触发都无法保证完全消除内存泄露的情况。
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/**
* The value associated with this ThreadLocal.
*/
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ArrayBlockingQueue(数组阻塞队列)类
数组阻塞队列类是BlockingQueue(阻塞队列)接口的主要实现类之一,采用数组的方式以实现。由于其迭代器在迭代过程中需要与本体进行数据互通,因此在数组阻塞队列中存在一条被称为迭代器链的链表用于保存所有的有效迭代器。而被用于作为迭代器容器,同时也是迭代器链基本单位的节点类便直接继承于弱引用类。
节点类如此设计的原因同样是为了在一定程度上减少内存泄露的情况。数组阻塞队列的迭代器可能因为某些被分离(即不再与数组阻塞队列进行数据互通)或抛弃,因此其无效节点理论上应该从迭代器链中移除,否则就会造成内存泄露的情况。但实际是从链表中移除某个无效节点并不轻松,需要先遍历迭代器链找到指定节点后才可移除。而由于数组阻塞队列类在设计上用于并发环境中,无效节点的产生可能比较频繁,并且由于当数组阻塞队列本身数据发生变化时也经常需要遍历迭代器链以和迭代器进行数据互通,因此直接遍历迭代器链进行移除必然会产生较大的性能消耗。为了避免这种性能消耗,同时又能减少内存泄露的情况,数组阻塞队列类令节点类继承了弱引用类,使得节点与迭代器之间成为了弱引用关系,即将迭代器作为了节点的所指对象来使用。故而一旦迭代器被抛弃就可以直接被GC回收,而不收到节点的影响。而残留在迭代器链中的节点则会在数组阻塞队列自行遍历迭代器链是被移除。
注意:与弱哈希引用类形同,基于半自动化及并发的原因半自动化的,无论清理是否被触发都无法保证完全消除内存泄露的情况。
/**
* Node in a linked list of weak iterator references.
* 弱迭代器引用链表的一个节点。
*
* @Description: 节点类:这个一个弱引用类,以迭代器作为其所指对象,一旦迭代器失去了外部引用被GC回收后,节点就会变成一个失效节
* @Description: 点,即没有迭代器的节点。
*/
private class Node extends WeakReference<Itr> {
/**
* @Description: 后继:用于持有迭代器链中后继节点的引用
*/
Node next;
Node(Itr iterator, Node next) {
super(iterator);
this.next = next;
}
}