你需要知道的四种java引用

在Java语言中,除了原始数据类型(int/float等基本数据类型)的变量,其他所有的引用类型,都是指向到内存中不同的对象(一般为new操作符创建的对象)。而为了更好的管理内存,java在1.2版本中增加了包java.lang.ref,这个包提供了三个引用对象的实现类:SoftReference、WeakReference、PhantomReference。即软引用、弱引用、虚引用。此外对于直接用new操作符创建的对象,如果该对象使用一个变量来引用,那么这种引用类型称为强引用。本篇文章将从对象的生命周期、GC以及Object#finalize()方法说起,认识一下这三种引用对象的作用和应用场景。

对象的生命周期及GC

为了说明强引用、软引用、弱引用、虚引用,我们有必要来了解一下对象的生命周期和GC的基本原理。下面就以一个不十分详细的说明来描述一下。(对于想深入了解的可以参见书籍《深入理解Java虚拟书》这本书)
假设我们有一段代码

1
2
3
4
5
public static void foo(String bar) {
Integer baz = new Integer(bar);
}
// 调用函数
foo("123")

传入参数bar给方法foo(String bar),该方法内部创建了一个Integer的对象。
下图描述了在java内存空间中,栈和堆的关系。可以看出,bar作为方法foo的入参,其栈内存放了一个变量bar,指向堆中的地址,字符串”123”在堆内存中的地址。在foo函数的执行栈中,baz变量同样在栈中存放了指向堆内存,即Integer实例的内存地址。
重点:在foo函数执行完毕后,弹出执行栈,其内部的baz、bar变量立即销毁,此时堆中的”123”、Integer(“123”)对象将没有任何引用,当JVM内存不足,触发GC时,这些内存空间将被回收。

梳理一下,对于一般情况(也可以理解为强引用的情况下),堆内存中的对象,当没有变量引用时,在GC周期它们所属的内存将被回收。但在软引用、弱引用的作用下,就算堆内存中的对象被引用类型的实例(即SoftReference或WeakReference)引用,仍然可以触发回收动作。

四种引用的官方介绍

引用官方的说法,在GC对实例进行可达性分析时,主要有四种可达性:

  1. strongly reachable,如果某个对象可以在不遍历任何引用对象的情况下到达某个对象,则该对象是strongly reachable
  2. softly reachable,如果某个对象无法通过strongly reachable,需要通过soft reference引用对象才可达,则该对象是soft reachable
  3. weakly reachable, 如果某个对象无法通过strongly、softly reachable,而只能通过weak reference引用对象才可达,则该对象是weakly-reachable,
    当清除weakly-reachable对象时,其中包含的实例将被清除
  4. phantom reachable,无法通过strongly、softly、weakly reachable可达的,则它要么是已经被finalize机制回收或者phantom reference实例引用着

四种引用对象提供了一种有限的能力用于跟GC打交道,一个程序可以使用一个引用对象来维护某个其他对象的引用,使得后者对象仍然可以在某种情况下被收集器来回收。这样讲,可能有点不明确,请看图和描述。

在引用对象(reference-object)中使用了一个实例变量引用了一个对象(referents),在这种情况下就算引用对象(reference-object)仍然处于作用域中,被引用对象仍然有可能被GC回收。各自的作用如官网所说:

软引用的应用场景

软引用,在JVM内存不足时,将回收软引用对象中被引用的对象。
应用场景:处理超大数据集时,避免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
public static List<List<Object>> processResults(ResultSet rslt)
throws SQLException
{
try
{
// 创建一个软引用对象,其中引用了一个LinkedList实例,用于保存处理结果
SoftReference<List<List<Object>>> ref
= new SoftReference<List<List<Object>>>(new LinkedList<List<Object>>());

ResultSetMetaData meta = rslt.getMetaData();
int colCount = meta.getColumnCount();

int rowCount=0;
while (rslt.next()){
rowCount++;
// store the row data

List<List<Object>> results = ref.get();
if (results == null)
throw new TooManyResultsException(rowCount);
else
results.add(row);
// 取消强引用,这样才能保证在内存不足时可以进行软引用的收集动作
results = null;
}

return results;
}
finally
{
closeQuietly(rslt);
}
}

弱引用应用场景

弱引用,当一个对象只存在弱引用时,在GC时将被回收
应用场景:

  1. 关联没有固有关系的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 固定关系的一端
    static class Person {

    // 当发生GC时,回收实例将调用该方法
    @Override
    protected void finalize() throws Throwable {
    System.out.println("Finalize");
    super.finalize();
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void test1() throws InterruptedException {
// 使用WeakHashMap来绑定两个对象的关系
Map<Person, String> personIds = new WeakHashMap<>();
Person person = new Person();
// WeakHashMap会将key包装为一个WeakReference对象
personIds.put(person, "1");

String s = personIds.get(person);
System.out.println(s);

// 去掉对实例的强引用之后,当前实例只存在弱引用,即WeakHashMap中对应的key
person = null;
// 手动触发GC,将会回收person实例
// 如果使用HashMap来保存关系的话,为了避免内存泄漏,我们需要手动HashMap#remove(person)
// 来删除引用,让GC回收person。使用弱引用后,将无需我们来管理这一部分内存管理
System.gc();

TimeUnit.SECONDS.sleep(10);
}
  1. 通过规范化的Map来减少重复
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 模拟String类的intern(String string)方法,来保证不会有重复对象
    private Map<String,WeakReference<String>> _map
    = new WeakHashMap<String,WeakReference<String>>();

    public synchronized String intern(String str)
    {
    WeakReference<String> ref = _map.get(str);
    String s2 = (ref != null) ? ref.get() : null;
    if (s2 != null)
    return s2;

    _map.put(str, new WeakReference(str));
    return str;
    }

虚引用的应用场景

虚引用(PhantomReference)不同于软引用、弱引用,其无法获得被引用对象,但是可以在引用对象被回收时,通过ReferenceQueue将自身(引用对象本身)来通知应用程序,进行一些回收资源的操作。
收到回收资源的操作,Java中有一个类似于C++的析构函数(Object#finalize)。如果在类中覆盖该方法,那么在GC回收该类的实例的时候,将触发调用该方法(GC通过一个线程来调用,时间复杂度为O(n)级别),在执行完这个方法之后,才会进行内存资源的回收。因此存在一个弊端,如果很多类都实现了finalize方法,那么会导致内存资源回收失败,发生OOM异常。

总结

  1. 四种引用:强引用、软引用、弱引用、虚引用
    2. 软引用、弱引用、虚引用在Java中各自的实现类,以及作用
    熟悉这四种引用的含义和作用,将可以帮助我们更好的了解和使用JVM内存,避免内存泄漏导致的OOM异常

参考

Java Reference Objects
官方文档
WeakReference