强引用、软引用、弱引用、幻想引用有什么区别

强引用、软引用、弱引用、幻想引用有什么区别

一月 07, 2020

概念

强引用

强引用就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还”活着”,垃圾收集器不会处理这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应引用赋值为null,就是可以被垃圾收集的了,当然具体回收时机还需要看垃圾收集策略。我们使用的大部分引用都是强引用,这是使用最普遍的引用。如:

1
String strongReference = new String("refenrence");

软引用

特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

1
2
3
4
5
6
7
String str = "软引用";
SoftReference softReference = new SoftReference(str);
str = null;
System.out.println(softReference.get());
System.gc();
System.runFinalization();
System.out.println(softReference.get());

上面的例子中两次都会正确输出”软引用”,因为软引用只会在JVM认为内存不足时才会清理。

弱引用

引用并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如维护一种非强制性的映射关系,如果试图获取时对象还在,就使用他否则就重新实例化。软引用适合做缓存而弱引用适合存储元数据。

1
2
3
4
5
6
7
String str = new String("弱引用");
WeakReference weakReference = new WeakReference(str);
str = null;
System.out.println(weakReference.get());
System.gc();
System.runFinalization();
System.out.println(weakReference.get());

上面的例子中只会输出一次”弱引用”,因为只要JVM运行GC发现有弱引用便会直接进行清理,并不会关注此时JVM内存是否充足。

幻想引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
3
4
5
6
7
8
String str = new String("幻象引用");
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(str, referenceQueue);
str = null;
System.out.println(phantomReference.get());
System.gc();
System.runFinalization();
System.out.println(referenceQueue.poll() == phantomReference);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。

应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知

引用队列

我们在创建各种引用并关联到相应对象中时,可以选择是否需要关联引用队列,JVM会在特定时机将引用放入引用队列中,我们可以从队列中获取引用进行相关后续逻辑。尤其是幻想引用,get方法返回null,如果不指定引用队列基本就没有意义了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try {
// Remove 是一个阻塞方法,可以指定 timeout,或者选择一直阻塞
Reference<Object> ref = refQueue.remove(1000L);
if (ref != null) {
// do something
}
} catch (InterruptedException e) {
// Handle it
}

诊断JVM引用情况

如果怀疑应用存在引用导致的回收问题,可以通过指定JVM参数来排查诊断:

1
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC

Reachability Fence

按照Java规范如果一个对象没有指向强引用就符合垃圾收集的标准,有些时候对象并没有强引用,但是也许它得部分属性还在被使用,这样就导致诡异的问题,所以我们需要一个方法,在没有强引用的情况下通知JVM对象是在被使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Resource {
private static ExternalResource[] externalResourceArray = ...
int myIndex; Resource(...) {
myIndex = ...
externalResourceArray[myIndex] = ...;
...
}
protected void finalize() {
externalResourceArray[myIndex] = null;
...
}
public void action() {
try {
// 需要被保护的代码
int i = myIndex;
Resource.update(externalResourceArray[i]);
} finally {
// 调用 reachbilityFence,明确保障对象 strongly reachable
Reference.reachabilityFence(this);
}
}
private static void update(ExternalResource ext) {
ext.status = ...;
}
}

方法action的执行依赖于对象的部分属性,所以被保护了起来。否则如果我们在代码中像这样调用

1
new Resource().action()

可能就会出现问题,如果没有强引用指向我们创建出来的Resource对象,JVM对它进行finalize是完全合法的。这在新的异步编程模式下可能会非常有用,可以保障对象不被意外回收。