JVM面试(七)-强引用、软引用、弱引用、虚引用及应用场景

强引用、软引用、弱引用、虚引用及应用场景

引用类型的作用

  • 可以 通过代码的方式 决定 某些对象的生命周期
  • 有利于JVM进行垃圾回收

强引用

Java中最常见的就是强引用
把 一个对象 赋给 一个引用变量 时,这个引用变量 就是一个强引用
有强引用的对象 一定为 可达性状态,所以不会被垃圾回收机制回收

强引用是造成Java内存泄漏的主要原因
如果想中断 强引用 和 某个对象 之间的关联关系,可以 显示地 将引用 赋值为 null,JVM就会在合适的时间回收该对象,例如 集合类中的clear方法,这里会牵扯到内存泄漏和内存溢出的区别

软引用-SoftReference

软引用通过SoftReference类实现
如果 一个对象 只有 软引用,则 在系统内存空间不足时 该对象 将被回收
在垃圾回收器没有回收之前,该对象都可以被程序使用

SoftReference的特点 是 它的一个实例 保存 对一个Java对象 的 软引用,该软引用的存在 不妨碍 垃圾收集线程 对 该Java对象 的回收
也就是说,一旦SoftReference 保存了 对一个Java对象 的 软引用后,在垃圾线程 对 这个Java对象 回收前,SoftReference类所提供的get()方法 返回Java对象 的 强引用

一旦 垃圾线程 回收了 该Java对象之后,get()方法将返回null

软可及对象 的 清理 是由 垃圾收集线程 根据其特定算法 按照 内存需求决定的

在创建软引用时,还可以传入ReferenceQueue(引用队列),当 JVM 回收 某个软引用对象 之后 会将 该SoftReference对象 添加进 这个队列,因此就可以知道 这个对象 啥时候被回收了,可以做一些想做的操作

    public SoftReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
        this.timestamp = clock;
    }

使用SoftReference做缓存

软引用在实际中有重要的应用,例如浏览器的后退按钮,这个后退时显示的网页内容可以重新进行请求或者从缓存中取出:

  • 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
  • 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出这时候就可以使用软引用

使用SoftReference作为缓存

import java.lang.ref.SoftReference;
import java.util.HashMap;

/**
 * SoftRefenceCache
 * @param <K> key的类型.
 * @param <V> value的类型.
 */
public class SoftReferenceCache<K, V> {
  private final HashMap<K, SoftReference<V>> mCache;

  public SoftReferenceCache() {
    mCache = new HashMap<K, SoftReference<V>>();
  }

  /**
   * 将对象放进缓存中,这个对象可以在GC发生时被回收
   * 
   * @param key key的值.
   * @param value value的值型.
   */

  public void put(K key, V value) {
    mCache.put(key, new SoftReference<V>(value));
  }

  /**
   * 从缓存中获取value
   * 
   * @param key
   *
   * @return 如果找到的话返回value,如果被回收或者压根儿没有就返* 回null
   */
   
  public V get(K key) {
    V value = null;

    SoftReference<V> reference = mCache.get(key);

    if (reference != null) {
      value = reference.get();
    }

    return value;
  }
}

弱引用-WeakReference

WeakReference是Java语言规范中 为了区别 直接的对象引用(程序中 通过构造函数 声明出来的对象引用)而定义的 另外一种引用关系
WeakReference标志性的特点是:reference实例(WeakReference对象) 不会影响到 被应用对象(T) 的 GC回收行为
即 只要 对象(T) 被 除WeakReference对象之外 所有的对象 解除引用 后,该对象(T) 便可以被GC回收
只不过 在对象(T) 被回收之后,reference实例(WeakReference对象) 想获得 被应用的对象(T)时 程序会返回null
这一段很关键!!!

弱引用通过WeakReference类实现
如果 一个对象 只有弱引用,则在 垃圾回收过程中 一定会被回收(无论内存是否充足,这是弱引用和软引用的区别!!!)
如果 存在强引用 同时与之关联,则在 进行垃圾回收时 也不会回收该对象(软引用也是如此)

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果 弱引用 所引用的对象 被JVM回收,这个弱引用 就会被加入到 与之关联的 引用队列中

应用场景:
如果一个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么应该用 Weak Reference 来记住此对象
或者想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候就应该用弱引用,这个引用不会在对象的垃圾回收判断中产生任何附加的影响

面试题:ThreadLocal为什么使用WeakReference?

ThreadLocal#getMap方法 返回 当前线程t的threadLocals变量
threadLocals的类型是 ThreadLocal类 的 静态内部类ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用
Entry声明为WeakReference,岂不是说,在某个任意时刻,GC都有可能把ThreadLocal对象对应的Value也给回收了?
因为 声明的WeakReference引用 指向的是 ThreadLocal对象,而value则是强引用类型

ThreadLocalMap 和 普通map的最大区别 就是 它的Entry 是针对 ThreadLocal弱引用的
即当ThreadLocal 没有 其他引用为空时,JVM就可以GC回收ThreadLocal,从而得到一个null的key


static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

当threadLocal实例 可以被GC回收时,系统可以检测到 该threadLoca l对应的 Entry 是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots)来自动做一些清除工作
否则如果不做清除工作的话 容易产生 内存无法释放的问题,因为Entry.value对应的对象 即使不再使用,但由于被threadLocalMap所引用 导致 无法被GC回收
实际代码中,ThreadLocalMap会在set,get方法中调用rehash方法,rehash方法调用resize方法,resize方法针对Entry中key=null的情况把对应的value也设置为null, set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots
当然将threadLocal对象设置为null 并不能完全避免 内存泄露对象,最安全的办法仍然是调用ThreadLocal的remove方法,来彻底避免可能的内存泄露

阅读参考:

虚引用-PhantomReference

虚引用通过PhantomReference类实现
虚引用 必须和 引用队列联合使用,主要用于 跟踪对象 的 垃圾回收状态

如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收
虚引用 主要用来跟踪 对象 被垃圾回收的活动

当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把 这个虚引用 加入到 与之关联的引用队列中
程序可以通过 判断引用队列中 是否已经加入了虚引用,来了解 被引用的对象 是否将要被垃圾回收
如果程序发现 某个虚引用 已经被加入到 引用队列,那么 就可以 在所引用的对象 的 内存 被回收之前 采取必要的行动
PhantomReference#get()方法永远返回空,不管对象有没有被回收

应用场景:
可以用来跟踪对象呗垃圾回收的活动
一般可以通过虚引用 达到 回收一些 非java内的一些资源 比如堆外内存的行为
例如:在 DirectByteBuffer 中,会创建一个PhantomReference的子类Cleaner的虚引用实例 用来引用 该DirectByteBuffer 实例
Cleaner创建时会添加一个Runnable实例,当 被引用的DirectByteBuffer对象 不可达 被垃圾回收时,将会执行Cleaner实例内部的Runnable实例的run方法,用来回收堆外资源

引用队列-ReferenceQueue

引用队列,在检测到适当的可到达性更改后,垃圾回收器 将 已注册的引用对象(Reference) 添加到 该队列中

如果在创建一个引用(Reference)对象时,指定了ReferenceQueue,那么当 引用对象(Reference) 指向的对象 达到 合适的状态(根据引用类型不同而不同)时,GC 会把 引用对象本身(Reference) 添加到这个队列中,方便处理它
因为引用对象(Reference) 指向的对象 GC会自动清理,但是 引用对象本身 也是对象(是对象就占用一定资源),所以需要自己清理

总结

引用类型 被回收时间 用途 生存时间
强引用 never 对象的一般状态 jvm停止运行时
软引用 内存不足时 对象缓存 直到内存不足时
弱引用 gc时 对象缓存 直到gc时
虚引用 unknow unknow unknow

阅读参考:

上一篇:每日一问:谈谈对 MeasureSpec 的理解


下一篇:并发——深入分析ThreadLocal的实现原理