Java中,我们不需要手动管理对象的生命周期,但是如果希望对象具备一定的生命周期的话,就必须合理运用Java中的不同引用类型。从Java SE2起,就为提供了四种引用类型:强引用、软引用、弱引用和虚引用。本文通过分析他们在JDK中的使用场景来阐述不同引用类型在实际中的作用。

Java中提供这四种引用类型主要有两个目的:第一是可以让程序员通过代码的方式决定某些对象的生命周期;第二是有利于JVM进行垃圾回收。

强引用(StrongReference)

概念&用法

强引用就是指在程序代码之中普遍存在的对象,使用 new 创建。

Object obj = new Object();

特性

只要某个对象有强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出 OutOfMemoryError 错误也不会回收这种对象。

如果想强行中断强引用和某个对象的关联,可以显示的将引用赋值为null,这样Jvm会在合适的时间回收该对象。

应用

JDK中的应用

ArrayList 中删掉一个元素后,整体向前移动,最后一个位置需要置空回收。

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

软引用(SoftReference)

概念&&用法

用来描述一些还有用但是并非必须的对象,在Java中用java.lang.ref.SoftReference类来表示。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

特性

对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

内存不足时等价于:

if(内存不足){
	obj = null ;
	System.gc();
}

可以通过 -XX:SoftRefLRUPolicyMSPerMB 参数控制回收的时机。

应用

浏览器网页缓存实例

Browser bs= new Browser();
Optional<SoftReference> sf = Optional.of(new SoftReference(bs));
// 没被回收直接获取
// 已被回收 重新创建
sf.orElse(new SoftReference(new Broswer())); 

JDK中的应用

Java Class里面 reflectionData 方法获取反射数据的缓存,如果 (rd = reflectionData.get()) != null 说明有缓存,直接返回,否则重新创建。

private ReflectionData<T> reflectionData() {
    SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
    int classRedefinedCount = this.classRedefinedCount;
    ReflectionData<T> rd;
    if (useCaches &&
        reflectionData != null &&
        (rd = reflectionData.get()) != null &&
        rd.redefinedCount == classRedefinedCount) {
        return rd;
    }
    // else no SoftReference or cleared SoftReference or stale ReflectionData
    // -> create and replace new instance
    return newReflectionData(reflectionData, classRedefinedCount);
}

与引用队列联合使用

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用(WeakReference)

概念&&用法

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

下面代码会让 obj 变成强引用。

Object newObject = wf.get();

特性

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

应用

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

图像缓存

假设我们要构建一个缓存,将大图像对象保存为值,将图像名称保存为键。我们想选择一个合适的Map实现来解决这个问题。

使用简单的HashMap不是一个好的选择,因为值对象可能占用大量内存。更重要的是,即使它们不再在我们的应用程序中使用,它们也永远不会通过GC进程从缓存中回收。

理想情况下,我们需要一个允许GC自动删除未使用对象的Map实现。当我们的应用程序中的任何地方没有使用大图像对象的键时,该对象将从内存中删除。

WeakHashMap<UniqueImageName, BigImage> map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image");
 
map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));
 
imageName = null;
System.gc();
 
await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);

JDK中的应用

  1. WeakHashMap 中的 Entry 继承了 WeakReference ,能自动清除未被使用的对象。其构造函数如下:
Entry(Object key, V value,
      ReferenceQueue<Object> queue,
      int hash, Entry<K,V> next) {
    super(key, queue);// 调用构造函数,key为弱引用
    this.value = value;
    this.hash  = hash;
    this.next  = next;
}
  1. ThreadLocalThreadLocalMap 里的 Entry 同样也是继承了 WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k); // 调用构造函数,k为弱引用
        value = v;
    }
}

二者的共同点都是key为弱引用,当其只有弱引用时,GC一定会对其进行回收,释放内存

虚引用(PhantomReference)

概念&&用法

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

特性

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它的 get() 方法永远返回 null

应用

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。PhantomReference 唯一的用处就是跟踪 referent 何时被 enqueue 到 ReferenceQueue 中。

虚引用使用场景

1、它允许你知道具体何时其引用的对象从内存中移除。
而实际上这是Java中唯一的方式。这一点尤其表现在处理类似图片的大文件的情况。当你确定一个图片数据对象应该被回收,你可以利用虚引用来判断这个对象回收之后在继续加载下一张图片。这样可以尽可能地避免可怕的内存溢出错误。

2、虚引用可以避免很多析构时的问题。finalize 方法可以通过创建强引用指向快被销毁的对象来让这些对象重新复活。然而,一个重写了 finalize 方法的对象如果想要被回收掉,需要经历两个单独的垃圾收集周期。在第一个周期中,某个对象被标记为可回收,进而才能进行析构。

但是因为在析构过程中仍有微弱的可能这个对象会重新复活。这种情况下,在这个对象真实销毁之前,垃圾回收器需要再次运行。因为析构可能并不是很及时,所以在调用对象的析构之前,需要经历数量不确定的垃圾收集周期。这就意味着在真正清理掉这个对象的时候可能发生很大的延迟。这就是为什么当大部分堆被标记成垃圾时还是会出现烦人的内存溢出错误。

总结

从 java 1.2 版本引入 java.lang.ref 包,共 4 种引用,这 4 种引用的级别高低依次为:

强引用 > 软引用 > 弱引用  > 虚引用 
引用级别GC生存时间
强引用从不对象的一般状态,JVM停止运行时终止
软引用内存不足时JVM内存不足时
弱引用垃圾回收时(只具有弱引用的对象)下一次垃圾回收发生之前
虚引用--

参考链接 :

Java中的强引用,软引用,弱引用,虚引用有什么用?

Guide to WeakHashMap in Java

Q.E.D.