目录
一、引用的概念
二、引用到底有什么作用
三、弱引用的 GC 实战
四、再理解 ThreadLocalMap 的弱引用
五、ReferenceQueue 引用队列
六、应用场景
总结
一、引用的概念
JDK 1.2 版之后引入了软(SoftReference)、弱(WeakReference)、虚(PhantomReference)三种引用。
强引用:最传统的「引用」的定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj=new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:描述一些还有用,但非必须的对象。只被软引用关联着的对象,在发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用:描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
二、引用到底有什么作用
假设我们有一个对象 Data ,还有一个对象 Entry 中依赖 Data 对象。伪代码如下:
class Data {
byte[] v;
}
class Entry {
Data d;
}
Data data = new Data(new byte[10 * 1024]);
Entry entry = new Entry(data);
如果在运行过程中,data = null 后,data 对象可以被垃圾回收掉吗?
答案是:需要看 entry 对象是否为 null
如果 entry 一直不为 null 的话,data 永远不能被回收,因为 Entry.d 变量引用了 data。
这时就可能发生内存泄漏。
那么如何解决呢,答案就是使用软、弱引用。
假如我们把 Entry 对 data 的依赖声明为一个软引用。如果 data = null 后,垃圾回收时就可以回收 data 对象了。
class Entry extends WeakReference<Data> {
public Entry(Data d) {
super(d);
}
}
我们可以大白话的理解为:
如果是弱引用,我对你的依赖很柔软薄弱,你觉得自己没有用了,我不会强行留住你,会放你走(垃圾回收)
如果是强引用,就算你觉得自己没有用了,我依然不让你走(不让垃圾回收)
比喻的总结四个引用
- 强引用:关系非常好,你自己没有用了,我也不会让你走。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
- 软引用:关系还行,你自己没有用了,我会挽留到在系统将要发生内存溢出异常前在走。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
- 弱引用:关系就那样,你自己没有用了,垃圾收集员一来你就可以走。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
- 虚引用:关系近乎敌人,我永远得不到你,垃圾收集员一来你就可以走。主要与 ReferenceQueue 配合使用,在回收时进行一些逻辑操作(认为是回收前执行一个回调函数)。
虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
我们可以看到,最主要还是「你自己没有用了」这个操作,可以认为是一个 obj = null 的操作。如果你走了,那么我也拿不到你的信息了。
三、弱引用的 GC 实战
@Slf4j
public class WeakReferenceExample {
public static void main(String[] args) throws InterruptedException {
// 10M 的缓存数据
byte[] cacheData = new byte[10 * 1024 * 1024];
// 将缓存数据用软引用持有
final WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
log.info("第一次 GC 前 {}", cacheData == null);
log.info("第一次 GC 前 {}", cacheRef.get() == null);
// 进行一次 GC 后查看对象的回收情况
System.gc();
Thread.sleep(1000); // 等待 GC
log.info("第一次 GC 后 {}", cacheData == null);
log.info("第一次 GC 后 {}", cacheRef.get() == null);
// 将缓存数据的强引用去除,你没有用了
cacheData = null;
System.gc();
Thread.sleep(1000); //等待 GC
log.info("第二次 GC 后 {}", cacheData == null);
log.info("第二次 GC 后 {}", cacheRef.get() == null);
}
/* 打印内容如下:
第一次 GC 前 false
第一次 GC 前 false
[GC (System.gc()) 14908K->11560K(125952K), 0.0318128 secs]
[Full GC (System.gc()) 11560K->11425K(125952K), 0.0216147 secs]
第一次 GC 后 false
第一次 GC 后 false
[GC (System.gc()) 12090K->11457K(125952K), 0.0016023 secs]
[Full GC (System.gc()) 11457K->818K(125952K), 0.0093186 secs]
第二次 GC 后 true
第二次 GC 后 true
*/
}
四、再理解 ThreadLocalMap 的弱引用
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
如果我们的代码中不需要 ThreadLocal 这个对象的话,即 ThreadLocal = null。但是 ThreadLocalMap 是线程的变量,如果线程一直运行,那么 ThreadLocalMap 永远不会为 null。
- 如果使用强引用,Entry 中的 k 强引用了 ThreadLocal ,ThreadLocal 永远不能释放。
- 如果使用弱引用,ThreadLocal 在垃圾回收时将释放,Entry 中的 k 将变为 null,这样线程中通过ThreadLocal 存入的数据就无法remove,若线程一直不执行,存储的数据就得不到释放,从而造成内存泄漏。
五、ReferenceQueue 引用队列
引用队列,用于存放待回收的引用对象。
对于软引用、弱引用和虚引用,如果我们希望当一个对象被垃圾回收器回收时能得到通知,进行额外的处理,这时候就需要使用到引用队列了。
在一个对象被垃圾回收器扫描到将要进行回收时 reference 对象会被放入其注册的引用队列中。我们可从引用队列中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理,资源释放等。
下面使用「虚引用」与「引用队列」实战说明 源码 :
- 创建一个 Map ,Key 是一个虚引用,虚引用关联 ReferenceQueue 队列,每当 Key 被回收时,这个 Key 会入队列。
- 起一个线程不停的取队列中的回收对象进行打印操作。
- 向 Map 循环 N 次,每次 put 一个大小为 1M 的字节数组,随着内存增长,垃圾回收器开始工作。
垃圾回收器工作时,可以看到队列中将被回收的对象信息。
@Slf4j
public class PhantomReferenceExample {
private static final ReferenceQueue<byte[]> RQ = new ReferenceQueue<>();
public static void main(String[] args) {
final Map<PhantomReference<byte[]>, Object> map = new HashMap<>();
final Thread thread = new Thread(() -> {
try {
int cnt = 0;
PhantomReference<byte[]> k;
while ((k = (PhantomReference<byte[]>) RQ.remove()) != null) {
log.info("第 {} 个回收对象,对象打印为:{}", cnt++, k);
}
} catch (InterruptedException ignored) {
}
});
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 1000; i++) {
map.put(new PhantomReference<>(new byte[1024 * 1024], RQ), new Object());
}
log.info("map.size :{}", map.size());
}
/* 部分输出如下:
* 第 789 个回收对象,对象打印为:java.lang.ref.PhantomReference@26653222
* 第 790 个回收对象,对象打印为:java.lang.ref.PhantomReference@553f17c
* 第 791 个回收对象,对象打印为:java.lang.ref.PhantomReference@56ac3a89
* 第 792 个回收对象,对象打印为:java.lang.ref.PhantomReference@6fd02e5
* 第 793 个回收对象,对象打印为:java.lang.ref.PhantomReference@2b98378d
* 第 794 个回收对象,对象打印为:java.lang.ref.PhantomReference@26be92ad
* 第 795 个回收对象,对象打印为:java.lang.ref.PhantomReference@6d00a15d
* map.size :1000
*/
}
一般情况我们很少使用软、弱、虚三种引用,如果使用请深入研究其利害,避免引起不必要的 Bug ,通常情况多用于缓存操作,防止缓存无限增长导致内存溢出。
六、应用场景
- WeakHashMap 实现类,如果 WeakHashMap 中的 Key 对象如果不需要了,WeakHashMap 内部可以配合 ReferenceQueue 引用队列进行移除。
- 缓存的实现,因为缓存一般情况会长时间存活,如果缓存的元素已经失效了,使用软弱引用配合 ReferenceQueue 引用队列可以执行清除操作。
- 使用虚引用,完成垃圾回收时的消息回调等操作。
总结
引用可区分为强、软、弱、虚四种,后三种可配合「引用队列」进行一些回收前的操作。