bboyjing's blog

Java强、软、弱、虚引用的直观理解

  众所周知,目前Java的引用分为强引用、软引用、弱引用和虚引用,这四种强度依次逐渐减弱。网上也能搜到很多相关文章,有的写的也很好,可能自身理解能力比较差,每次看过之后总觉得差点意思,本章准备通过几个小例子来加深理解。下面先简单看一下四种引用的定义:

  • 强应用:只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用:只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

这是从《深入理解Java虚拟机》第三版中摘抄出来的,下面就用例子来证明上述定义,同时加深对其的理解。

强引用

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
38
/**
* vm args: -Xmx11m
*
*/
public class StrongReferenceTest {
public static void main(String[] args) {
// -Xmx11m,正好够生成a、b两个对象
A a = new A();
B b = new B(a);
// 将栈帧中的局部变量a指向null,释放对堆上A对象实例的强引用
// 但是b仍然持有对堆上A对象实例的强引用
a = null;
// 再次构造一个A对象,报OOM
A newA = new A();
}
static class A {
int[] a;
public A() {
a = new int[1024 * 1024];
}
}
static class B {
A a;
public B(A a) {
this.a = a;
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at cn.didadu.sample.jvm.reference.StrongReferenceTest$A.<init>(StrongReferenceTest.java:29)
at cn.didadu.sample.jvm.reference.StrongReferenceTest.main(StrongReferenceTest.java:22)

  上述代码的意图是:固定堆的大小正好够创建A、B两个对象的实例,同时B对象强引用A对象。运行过程参照注释,最后当再次想创建一个A对象实例时,因为之前创建的A对象实例仍然被强引用着,此时堆没有空间再创建新对象,所以报出OutOfMemoryError。

软引用

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
/**
* vm args: -Xmx11m
*
*/
public class SoftReferenceTest {
public static void main(String[] args) {
// -Xmx11m,正好够生成a、b两个对象
A a = new A();
B b = new B(a);
// 将栈帧中的局部变量a指向null,释放对堆上A对象实例的强引用
// 这一步之后,意味着堆上A对象实例没有强引用了,只有b对它的软引用
a = null;
// 再次构造一个A对象,成功
A newA = new A();
}
static class A {
int[] a;
public A() {
a = new int[1024 * 1024];
}
}
static class B {
SoftReference<A> softA;
public B(A a) {
this.softA = new SoftReference<>(a);
}
}
}

  上述代码的意图是:固定堆的大小正好够创建A、B两个对象的实例,同时B对象软引用A对象。运行过程参照注释,最后当再次想创建一个A对象实例时,因为之前创建的A对象实例只有一个软引用与之关联,此时堆没有空间再创建新对象,会触发GC回收A对象实例,所有可以成功再创建出另一个新的A对象实例。

弱引用

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
public class WeekReferenceTest {
public static void main(String[] args) {
A a = new A();
B b = new B(a);
// 将栈帧中的局部变量a指向null,释放对堆上A对象实例的强引用
// 这一步之后,意味着堆上A对象实例没有强引用了,只有b对它的弱引用
a = null;
// 执行GC,假设GC能及时响应
System.gc();
// 由于堆上的A对象实例只有弱引用,GC时被垃圾回收了
// 所以,b.getA()将返回null
System.out.println(b.getA());
}
static class A {
}
static class B {
WeakReference<A> weakA;
public B(A a) {
weakA = new WeakReference<>(a);
}
public A getA() {
return weakA.get();
}
}
}

上述代码的意图是:不用再固定堆的大小,默认就好,B对象弱引用A对象,运行过程参照注释,最终输出null,只有弱引用的A对象实例被回收了。

虚引用

  虚引用必须和引用队列(ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。