八一八强引用、软引用、弱引用、虚引用

背景

我们都知道JVM的垃圾回收机制中,GC判断堆中的对象实例或者数据是不是需要回收(栈里面存放的是基本类型数据及对像的引用,所以是不需要GC去回收)的方法主要有两种:引用计数法和可达性算法两种。而无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”相关。

引用

先说说引用,Java中的引用,类似C语言中的指针。都知道Java分为基本类型和引用类型。有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的。JAVA中一切皆对象,无论你是直接操作对象本身,还是通过指向对象的引用来间接操作对象,都是采用统一的一种方法。

在 JDK 1.2 之前,Java 中的引用的定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该 refrence 数据是代表某块内存、某个对象的引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。比如我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为:

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)
    这四种引用强度依次逐渐减弱,Java中引入这四种引用的目的是让程序自己决定对象的生命周期,JVM通过垃圾回收器对这四种引用做不同的处理,来实现对象生命周期的改变。

    其中FinalReference类是包内可见,其它三种引用类型均为public,可以在应用程序中直接使用。

强引用

在Java中最常见的就是强引用,就是直接new出来的对象都是强引用,例如:

1
2
// person就是一个强引用
Person person = new Person()

当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收器回收的,即使该对象用用不会被用到也不会被回收。当内存不足,JVM开始垃圾回收,对于强引用的对象,就算出现了OOM也不会对该对象进行回收,因此强引用有时也是造成Java内存泄漏的原因之一。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应引用赋值为null,一般就认为是可以被GC回收的。

1
2
3
4
5
6
7
8
9
10
public class StrongRefenenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
Object o2 = o1;
o1 = null;
System.gc();
System.out.println(o1); //null
System.out.println(o2); //java.lang.Object@2503dbd3
}
}

这里尽管o1已经被回收了,但是o2强引用了o1,所以不会被GC回收。o1指向的内存为null,但是o2指向的原来o1的内存对象是不会被回收的。

软引用

软引用是一种相对强引用弱化了一些的引用。它用来描述一些还有用,但是并非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。

对于只有软引用的对象来说:当系统内存充足时它不会被回收,当系统内存不足时它才会被回收。看例子:

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
26
27
28
29
30
31
32
33
34
35
36
37
public class JavaReference {

public static void main(String[] args) {

softRefMemoryEnough();
System.out.println("------内存不够用的情况------");
softRefMemoryNotEnough();
}

private static void softRefMemoryEnough() {

Object o1 = new Object();
SoftReference s1 = new SoftReference(o1);
System.out.println(o1);
System.out.println(s1.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(s1.get());//不会被回收
}

/**
* JVM配置`-Xms5m -Xmx5m` ,然后故意new一个一个大对象,使内存不足产生 OOM,看软引用回收情况
*/
private static void softRefMemoryNotEnough() {
Object o1 = new Object();
SoftReference s1 = new SoftReference(o1);
System.out.println(o1);

System.out.println(s1.get());
o1 = null;
byte[] bytes = new byte[10 * 1024 * 1024];
System.out.println(o1);
System.out.println(s1.get()); //会被回收
}

}

软引用通常在对内存敏感的程序中,比如告诉缓存就有用到软引用,内存够用的时候就保留,不够用就回收。Android中图片库Glide就是用到软引用,还比如Mybatis缓存类SoftCache用到软引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object getObject(Object key) {
Object result = null;
SoftReference softReference = (SoftReference)this.delegate.getObject(key);
if (softReference != null) {
result = softReference.get();
if (result == null) {
this.delegate.removeObject(key);
} else {
synchronized(this.hardLinksToAvoidGarbageCollection) {
this.hardLinksToAvoidGarbageCollection.addFirst(result);
if (this.hardLinksToAvoidGarbageCollection.size() > this.numberOfHardLinks) {
this.hardLinksToAvoidGarbageCollection.removeLast();
}
}
}
}
return result;
}

弱引用

弱引用也是用来描述非必需对象的,但是它的强度比软引用还要更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用需要用WeakReference类来实现,它比软引用的生存周期更短。

1
2
3
4
5
6
7
8
9
10
private static void weakRefDemo() {
Object o1 = new Object();
WeakReference w1 = new WeakReference(o1);
System.out.println(o1);
System.out.println(w1.get());
o1 = null;
System.gc();
System.out.println(o1); //被回收
System.out.println(w1.get());//被回收
}

官方文档说弱引用常被用来实现规范化映射,JDK中的WeakHashMap就是一个这样的例子(ThreadLocal中也用到弱引用)

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
26
public static void myHashMap() {
HashMap map = new HashMap();
String key = new String("k1");
String value = "v1";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);//不会被回收
}

public static void myWeakHashMap() throws InterruptedException {
WeakHashMap map = new WeakHashMap();
//String key = "weak"; //key指向一个JVM字符串常量池中的"weak"字符串
// 刚开始写成了上边的代码
//思考一下,写成上边那样会怎么样? 那可不是引用了
String key = new String("weak");
String value = "map";
map.put(key, value);
System.out.println(map);
//去掉强引用
key = null;
System.gc();
Thread.sleep(1000);
System.out.println(map);//被回收,因为只被一个弱引用引用(WeakHashMap的原理)
}

虚引用

虚引用也称为“幽灵引用”,是一种最弱的引用关系。顾名思义,就是形同虚设,与其它集中引用不太一样,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。如果一个对象仅持有虚引用,那么它就和没有任务引用一样,在任何时候都可能被GC回收,它不能单独使用也不能通过它来访问对象,虚引用必须和引用队列(RefenenceQueue)联合使用。虚引用主要作用是跟踪对象垃圾回收的状态,仅仅是提供一种确保对象finalize后可以做某些事情的机制。

PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被GC回收,用来实现比finalization机制更灵活的回收操作。

设置虚引用的唯一目的,就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理。

Java允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 虚引用
*/
private static void phantomRefDemo() throws InterruptedException {
Object o1 = new Object();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(o1,referenceQueue);
System.out.println(o1);
System.out.println(referenceQueue.poll());

System.out.println(phantomReference.get());
o1 = null;
System.gc();
Thread.sleep(3000);
System.out.println(o1);
System.out.println(referenceQueue.poll()); //引用队列中
System.out.println(phantomReference.get());
}

引用队列

ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。SoftReference,WeakReference,PhantomReference都有一个可以传递ReferenceQueue的构造器。创建引用的时候,可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于是一种通知机制。