Java 虚拟机入门教程之四 JVM 四种引用

大纲

Java 中四种的引用

在 Java 中有四种引用,分别是:强引用、软引用、弱引用、虚引用。伴随这四种引用一起使用的,还有 WeakHashMap、ReferenceQueue(引用队列)等。

引用的整体架构

当谈到一个类的实例化时,比如 Person p = new Person(),在等号的左边,就是一个对象的引用,存储在栈中;而等号的右边,就是实例化的对象,存储在堆中,其实这样的一个引用关系,就被称为 “强引用”。

强引用 (Reference)

强引用的概念

当内存不足的时候,JVM 开始执行垃圾收集,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收。强引用是最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还 “活着”,垃圾收集器不会回收这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,比如 Person p = new Person()当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾收集机制回收的,即使该对象以后永远都不会被使用到,JVM 也不会回收它,因此强引用是造成 Java 内存泄漏的主要原因之一。对于一个普通的对象,如果没有其它的引用关系,只要超过了引用的作用域或者显式地将相应的引用(强引用)赋值为 Null,一般可以认为它就是可以被垃圾收集器回收的(当然具体回收的时机还是要看垃圾收集策略),但这并不是绝对的。

强引用的使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ReferenceDemo1 {

public static void main(String[] args) {
// 这样定义的默认就是强引用
Object object1 = new Object();

// 创建第二个强引用,指向刚刚创建的 Object 对象
Object object2 = object1;

// 主动让对象成为垃圾
object1 = null;

// 手动GC
System.gc();

System.out.println(object1);
System.out.println(object2);
}

}

程序执行的输出结果:

1
2
null
java.lang.Object@2ff4f00f

从上述的输出结果可以发现,即使 obj1 被设置成了 null,然后调用 System.gc() 方法进行垃圾收集,但是也没有回收实例化出来的对象,obj2 还是能够指向该对象的内存地址,也就是说垃圾收集器并没有对该对象(强引用)进行垃圾收集。

软引用 (SoftReference)

软引用的概念

软引用是一种相对弱化了一些的引用,需要用 Java.lang.ref.SoftReference 类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:

  • 当系统内存充足时,它不会被回收
  • 当系统内存不足时,它会被回收

软引用通常用在对内存敏感的程序中,比如高速缓存就用到了软引用,在内存够用的时候就保留它,内存不够用就回收它。特别注意的是,GC 操作不会造成软引用指向的对象被垃圾收集器回收,即只有在内存不足(比如发生 OOM)的时候,才会被回收。

软引用的使用案例

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.lang.ref.SoftReference;

public class ReferenceDemo2 {

/**
* 内存够用的时候
*/
public static void softRef_Memory_Enough() {
// 创建一个强引用
Object obj1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(obj1);
System.out.println(obj1);
System.out.println(softReference.get());

// 主动让对象成为垃圾
obj1 = null;

// 手动GC
System.gc();

System.out.println(obj1);
System.out.println(softReference.get());
}

/**
* 内存不够用的时候
*/
public static void softRef_Memory_NotEnough() {
// 创建一个强引用
Object obj1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(obj1);
System.out.println(obj1);
System.out.println(softReference.get());

obj1 = null;

try {
// 创建一个 50M 的大数组对象,模拟自动 GC 并触发 OOM
byte[] bytes = new byte[50 * 1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(obj1);
System.out.println(softReference.get());
}
}

/**
* Java应用的JVM启动参数 -XX:+PrintGCDetails -Xms10m -Xmx10m
*/
public static void main(String[] args) {
softRef_Memory_Enough();
System.out.println("----------------------");
softRef_Memory_NotEnough();
}

}

程序执行的输出结果:

1
2
3
4
5
6
7
8
9
java.lang.Object@5ba23b66
java.lang.Object@5ba23b66
null
java.lang.Object@5ba23b66
----------------------
java.lang.Object@2ff4f00f
java.lang.Object@2ff4f00f
null
null

在上述代码中,分别写了两个方法,一个是内存够用的时候,一个是内存不够用的时候。首先在内存够用的时候,输出的是强引用的 obj1 和软引用的 softReference,它们都能够看到值(如下)。然后将 obj1 设置为 Null,并手动执行 GC 后,可以发现 softReference 的值还存在,说明在内存足够的时候,软引用的对象不会被垃圾收集器回收。

1
2
3
4
java.lang.Object@5ba23b66
java.lang.Object@5ba23b66
null
java.lang.Object@5ba23b66

当内存不够用的时候,由于使用了 JVM 启动参数配置,给初始堆内存的大小设置为 10M,最大堆内存大小也设置为 10M

1
-XX:+PrintGCDetails -Xms10m -Xmx10m

因此在创建一个 50M 的大数组对象时,必然会触发垃圾收集(GC),而且由于堆内存不足,紧接着就会导致 OOM 现象的出现

1
byte[] bytes = new byte[50 * 1024 * 1024];

最后看输出的结果可以发现(如下),obj1 和 softReference 都被垃圾收集器回收了。因此说明,软引用在内存不够用的时候,会被自动回收。

1
2
3
4
java.lang.Object@2ff4f00f
java.lang.Object@2ff4f00f
null
null

弱引用 (WeakReference)

弱引用的概念

弱引用不管内存是否够,只要有 GC 操作就会被垃圾收集。弱引用需要使用 java.lang.ref.WeakReference 类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要垃圾收集机制一运行,不管 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
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class ReferenceDemo3 {

public static void main(String[] args) {
// 创建一个强引用
Object obj1 = new Object();
// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(obj1);
System.out.println(obj1);
System.out.println(weakReference.get());

// 主动让对象成为垃圾
obj1 = null;

// 手动GC
System.gc();

System.out.println(obj1);
System.out.println(weakReference.get());
}

}

程序执行的输出结果:

1
2
3
4
java.lang.Object@2ff4f00f
java.lang.Object@2ff4f00f
null
null

从上述的输出结果可以发现,代码中并没有发生 OOM 内存溢出,只是手动执行了一次 GC 操作,弱引用指向的对象的堆内存空间就被垃圾收集器回收了。这就说明,弱引用不管内存是否够,只要有 GC 操作就会被垃圾收集。

WeakHashMap

WeakHashMap 的概念

比如一些常常和缓存打交道的框架(如 MyBatis 等),底层都应用到了 WeakHashMap。在 Java 中,WeakHashMap 跟 HashMap 类似,只不过它的 Key 是使用了弱引用类型的,也就是说,当执行 GC 的时候,HashMap 中的 Key 会被回收。WeakHashMap 的作用主要是在需要建立一种映射关系,而且希望在键不再被强引用持有时自动移除对应的映射关系时使用。WeakHashMap 典型应用场景包括缓存、临时映射等。当 Map 的某个键不再被外部持有强引用时,该键及其对应的值会被自动移除,这样可以避免内存泄漏,同时也减少了手动清理缓存的工作。

WeakHashMap 的使用案例
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;

public class ReferenceDemo4 {

public static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map + ", size = " + map.size());

// 主动让对象成为垃圾
key = null;
System.out.println(map + ", size = " + map.size());

// 手动GC
System.gc();
System.out.println(map + ", size = " + map.size());
}

public static void myWeakHashMap() {
// WeakHashMap 使用
Map<Integer, String> map = new WeakHashMap<>();
Integer key = new Integer(2);
String value = "WeakHashMap";
map.put(key, value);
System.out.println(map + ", size = " + map.size());

// 主动让对象成为垃圾
key = null;
System.out.println(map + ", size = " + map.size());

// 手动GC
System.gc();

// 等5秒再打印Map的大小
try {
System.out.println("wait ...");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(map + ", size = " + map.size());
}

public static void main(String[] args) {
myHashMap();
System.out.println("--------------");
myWeakHashMap();
}

}

程序执行的输出结果:

1
2
3
4
5
6
7
8
{1=HashMap}, size = 1
{1=HashMap}, size = 1
{1=HashMap}, size = 1
--------------
{2=WeakHashMap}, size = 1
{2=WeakHashMap}, size = 1
wait ...
{}, size = 0

从上述输出的结果可以看出,对于普通的 HashMap 来说,Key 置空不会产生任何影响,因为 HashMap 的 Key 默认是属于强引用,不会被垃圾收集器回收。但是对于 WeakHashMap 来说,Key 置空会产生重要影响,它的 Key 默认是弱引用,也就是在执行 GC 操作后,Key 会自动被垃圾收集器回收。

引用队列 (ReferenceQueue)

引用队列的概念

引用队列(ReferenceQueue)是用于配合软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)的一种机制。它的主要作用是允许程序员在对象被垃圾收集器回收之前执行一些特定的操作,比如清理资源或者记录对象被回收的情况。具体来说,当创建软引用、弱引用或虚引用时,可以将它们注册到一个 ReferenceQueue 实例中。当被引用的对象被垃圾收集器回收时,对应的引用对象会被放入 ReferenceQueue 中。这样一来,程序员就可以通过轮询或者在单独的线程中获取 ReferenceQueue 中的队列元素,来获知哪些对象已经被回收。

引用队列的使用案例

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
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class ReferenceDemo5 {

public static void main(String[] args) throws InterruptedException {
// 创建一个强引用
Object obj1 = new Object();

// 创建一个引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(obj1, referenceQueue);

System.out.println(obj1);
System.out.println(weakReference.get());
System.out.println((WeakReference<Object>) referenceQueue.poll());

System.out.println("------------");

// 主动让对象成为垃圾
obj1 = null;

// 手动GC
System.gc();

// 等待1秒
Thread.sleep(1000);

System.out.println(obj1);
System.out.println(weakReference.get());
System.out.println((WeakReference<Object>) referenceQueue.poll());
}

}

程序执行的输出结果:

1
2
3
4
5
6
7
java.lang.Object@2ff4f00f
java.lang.Object@2ff4f00f
null
------------
null
null
java.lang.ref.WeakReference@c818063

从上面的输出结果可以看出来,当手动执行 GC 操作之后,弱引用指向的对象会被垃圾收集器回收,且弱引用的实例在回收之前会被放入引用队列中保存,后续可以通过引用队列进行一些后置的操作。

虚引用 (PhantomReference)

虚引用的概念

虚引用又称为 “幽灵引用”,需要使用 java.lang.ref.PhantomReference 类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会影响对象的生命周期,它的作用仅限于在对象被回收之前通知程序进行必要的处理。如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾收集器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 ReferenceQueue 配合一起使用。虚引用的主要作用是跟踪对象被垃圾收集的状态,仅仅是提供一种确保对象被调用 finalize() 方法以后,做某些事情的机制。PhantomReference 的 get() 方法总是返回 Null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 GC 回收了,用来实现比 finalization 机制更灵活的回收操作。换句话说,设置虚引用关联的唯一目的,就是在这个对象被垃圾收集器回收之前,让程序收到一个系统通知或者后续执行进一步的处理。Java 技术允许调用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前,做必要的清理工作(如关闭文件、释放网络连接等),这个就相当于 Spring AOP 里面的后置通知。简而言之,虚引用一般用于在对象被垃圾收集器回收之前,做通知相关操作。

虚引用的使用案例

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
public class ReferenceDemo6 {

public static void main(String[] args) throws InterruptedException {
// 创建一个强引用
Object obj1 = new Object();

// 创建一个引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

// 创建一个虚引用
PhantomReference<Object> phantomReference = new PhantomReference<>(obj1, referenceQueue);

System.out.println(obj1);
System.out.println(phantomReference.get()); // 总是返回 null
System.out.println((PhantomReference<Object>) referenceQueue.poll());

System.out.println("------------");

// 主动让对象成为垃圾
obj1 = null;

// 手动GC
System.gc();

// 等待1秒
Thread.sleep(1000);

System.out.println(obj1);
System.out.println(phantomReference.get()); // 总是返回 null
System.out.println((PhantomReference<Object>) referenceQueue.poll());
}

}

程序执行的输出结果:

1
2
3
4
5
6
7
java.lang.Object@2ff4f00f
null
null
------------
null
null
java.lang.ref.PhantomReference@c818063

从上面的输出结果可以看出来,当手动执行 GC 操作之后,虚引用指向的对象会被垃圾收集器回收,且虚引用的实例在回收之前会被放入引用队列中保存,后续可以通过引用队列进行一些后置的操作。

各种引用的适用场景

适用场景介绍

  • 软引用(Soft Reference)

    • 软引用适用于需要缓存数据的场景。软引用的对象会在 JVM 内存不足时才会被回收,这使得它们非常适合用来实现缓存。当内存不足时,JVM 会优先回收软引用对象,从而释放内存。
    • 软引用通常用于缓存数据结构,如缓存图片、文件等,以便在内存不足时释放部分缓存占用的内存空间来避免 OutOfMemoryError。
  • 弱引用(Weak Reference)

    • 弱引用适用于临时持有对象,但不希望因为强引用导致对象无法被垃圾收集的场景。弱引用的对象在下一次垃圾收集时就会被回收。
    • 弱引用通常用于避免内存泄漏,例如在实现缓存时,如果一个对象不再被强引用持有,则它可以被及时地回收。
  • 虚引用 (PhantomReference)

    • 对象生命周期追踪:虚引用主要用于跟踪对象的垃圾收集状态。当一个对象仅有虚引用关联时,在垃圾回收器准备回收该对象时,会将其加入引用队列中。这使得程序能够在对象被回收之前做一些必要的处理工作。
    • 本地内存管理:在某些情况下,Java 对象可能需要与本地资源关联,比如 JNI(Java Native Interface)中。虚引用可用于通知 Java 对象在本地资源被释放时进行相应的清理操作,以避免资源泄漏。
    • 对象终结操作(Finalization)的替代:在 Java 中,一般通过调用 finalize() 方法来执行对象终结操作。但是 finalize() 方法的执行时机不确定,而且官方不推荐使用。虚引用提供了更灵活、可控的方式来执行类似的操作,避免了 finalize() 方法的一些问题。
  • 引用队列(ReferenceQueue)

    • 清理资源:当某个对象的软引用、弱引用或虚引用被放入 ReferenceQueue 中时,可以在相应的引用对象中保存对资源的引用,并在引用被加入
      到 ReferenceQueue 后,通过轮询或者监听 ReferenceQueue 来清理资源。
    • 记录对象被回收的情况:可以在对象被加入到 ReferenceQueue 后进行记录,以便了解系统中对象的回收情况,进而进行性能分析或者优化。
  • WeakHashMap

    • WeakHashMap 适用于缓存、临时映射等场景。
    • 当 Map 的某个键不再被外部持有强引用时,该键及其对应的值会被自动移除,这样可以避免内存泄漏,同时也减少了手动清理缓存的工作。

总结

总的来说,软引用适用于需要缓存且可以容忍一定程度内存占用的场景,而弱引用适用于需要临时持有对象且不希望影响垃圾收集的场景。

真实业务场景

假如有一个应用需要读取大量的本地图片,然后进行图片处理,比如裁剪尺寸大小、加水印等

  • 如果每次读取图片都是从硬盘读取,则会严重影响性能
  • 如果一次性将全部图片都加载到内存中,又可能造成 OOM 内存溢出

这时候使用软引用可以解决上述的问题。设计思路是,使用 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系(代码如下),在内存不足时,JVM 会自动回收这些缓存图片对象所占的堆内存空间,从而有效地避免了 OOM 的问题。当软引用关联的图片对象失效后(被 GC 回收了),可以从硬盘中再次将图片读取出到内存中。

1
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

提示

MyBatis 的底层源码大量使用到了软引用,有兴趣的可以细读一下 MyBatis 的源码实现。

GCRoots 和四种引用总结

  • 红色部分:在垃圾回收之外,也就是属于强引用
  • 蓝色部分:属于软引用,在内存不足的时候(比如发生 OOM),才会被回收
  • 弱引用:在每次垃圾收集器执行垃圾回收(GC)的时候,都会被回收
  • 虚引用:不会影响对象的生命周期,但在对象被垃圾收集器回收(GC)之前,引用实例会被存入引用队列中,可以通过引用队列实现一些通知机制