蹲厕所的熊

benjaminwhx

大话Java中的4种引用类型

2018-05-19 作者: 吴海旭


  1. 含义
    1. 强引用
    2. 软引用
    3. 弱引用
    4. 虚引用
  2. 原理
  3. 使用
    1. 使用软引用作为高速缓存
    2. 使用WeakHashMap

记得之前研究 Object.finalize() 的时候,研究过 FinalReference ,顺便也看了这4种引用。前段时间研究ThreadLocal和WeakedHashMap的时候又引出了弱引用,是时候该好好总结一下了。我们先来看看4种引用分别对应的含义是什么。

含义

强引用

强引用不会被GC回收,并且在java.lang.ref里也没有实际的对应类型,平时工作接触的最多的就是强引用。Object obj = new Object(); 这里的obj引用便是一个强引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用

如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。(具体什么时间回收可以看另一篇博客的原理分析),只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new SoftReference(reference);
reference = null; // MyObject对象只有软引用
System.gc();
System.out.println(root.get());

弱引用

如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回 收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

Object reference = new MyObject();
System.out.println(reference);
Reference root = new WeakReference(reference);
reference = null; // MyObject对象只有弱引用
System.gc(); 
System.out.println(root.get());

虚引用

虚引用主要用来跟踪对象被垃圾回收器回收的活动。它和之前两种引用的最大不同是:它的get方法一直返回null。

当垃圾回收器回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。虚引用的使用场景很窄,在JDK中,目前只知道在申请堆外内存时有它的身影。

申请堆外内存时,在JVM堆中会创建一个对应的Cleaner对象,这个Cleaner类继承了PhantomReference,当DirectByteBuffer对象被回收时,可以执行对应的Cleaner对象的clean方法,做一些后续工作,这里是释放之前申请的堆外内存。

由于虚引用的get方法无法拿到真实对象,所以当你不想让真实对象被访问时,可以选择使用虚引用,它唯一能做的是在对象被GC时,收到通知,并执行一些后续工作。

原理

上述引用中,除了强引用,其它几种都有对应的实现类,都继承了Reference,所有的精华也都在这个类中。

Reference有几个重要的参数,有些和GC密切相关:

  1. referent: 就是所引用的对象,会被GC特别对待。
  2. queue:RererenceQueue,看名字也知道它是一个Reference队列,用来保存Reference对象,当新建一个Reference时,可以选择性的传入第二个参数。
  3. discovered:该对象被JVM使用,表示下一个要被处理的Reference对象(1.8的实现)
  4. next:当Reference对象被放入RererenceQueue时,使用next变量形成链表结构。
  5. pending:该对象会被JVM使用,当前被处理的Reference对象。

Reference中有一个重要的线程 Reference Handler,运行优先级极高,启动之后负责轮询pending变量是否有数据,如果pending被JVM设置了一个值,就把它拿出来放到queue中,这里有个例外,就是之前说的堆外内存申请时的Cleaner对象,只会执行它的clean方法,并不会放到queue中。

当Reference对象被放进queue之后,就可以使用一个线程,依次拿出来进行处理。

文字太干,还是需要上代码:

final ReferenceQueue queue = new ReferenceQueue();
new Thread(new Runnable() {
  @Override
  public void run() {
      while (true) {
          try {
              Reference reference = queue.remove();
              System.out.println(reference + "回收了");
          } catch (InterruptedException e) {

          }
      }
  }
}).start();

Object o = new Object();
Reference root = new WeakReference(o, queue);
System.out.println(root);
o = null;
System.gc();
System.in.read();

上述代码中,先初始化了一个ReferenceQueue,随后又初始化了一个线程,循环的从queue中捞数据,因为当一个软引用、弱引用或虚引用的对象被GC回收时,这个引用会被放到对应的ReferenceQueue中,这里会被拿出来进行打印,更多的是做一些清理工作。

执行上述代码的结果:

java.lang.ref.WeakReference@34374ed5
0.174: [Full GC (System) 0.175: [CMS: 0K->437K(12288K), 0.0146570 secs] 1231K->437K(19712K), [CMS Perm : 2692K->2690K(21248K)], 0.0147430 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
java.lang.ref.WeakReference@34374ed5 回收了

使用

使用软引用作为高速缓存

public interface Cache<K, V> {
    boolean set(K key, V value);
    V get(K key);
}

public abstract class SoftReferenceCache<K, V> implements Cache<K, V> {

    private Map<K, ExtraInfoReference<V>> cache = new ConcurrentHashMap<>();
    private ReferenceQueue<V> referenceQueue = new ReferenceQueue<>();

    @Override
    public boolean set(K key, V value) {
        ExtraInfoReference<V> extraInfoReference = new ExtraInfoReference<>(key, value, referenceQueue);
        cache.put(key, extraInfoReference);
        return true;
    }

    @Override
    public V get(K key) {
        V value = null;
        if (cache.containsKey(key)) {
            ExtraInfoReference<V> sr = cache.get(key);
            value = sr.get();
        }

        if (value == null) {
            clear();
            value = getValueFromDB(key);
            ExtraInfoReference<V> extraInfoReference = new ExtraInfoReference<V>(key, value, referenceQueue);
            cache.put(key, extraInfoReference);
        }
        return value;
    }

    protected abstract V getValueFromDB(K key);

    private void clear() {
        ExtraInfoReference<V> reference = null;
        while ((reference = (ExtraInfoReference<V>) referenceQueue.poll()) != null) {
            Object key = reference.getKey();
            cache.remove(key);
            System.out.println("删除key: " + key);
        }
    }

    private static class ExtraInfoReference<T> extends SoftReference<T> {
        private Object key;

        public ExtraInfoReference(Object key, T value, ReferenceQueue<T> referenceQueue) {
            super(value, referenceQueue);
            this.key = key;
        }

        public Object getKey() {
            return key;
        }
    }
}

使用WeakHashMap

由于WeakHashMap的键对象为弱引用,因此当发生GC时键对象所指向的内存空间将被回收,被回收后再调用size、clear或put等直接或间接调用私有expungeStaleEntries方法的实例方法时,则这些键对象已被回收的项目(Entry)将被移除出键值对集合中。

【Effective Java】第6节中写到:内存泄露的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正好要实现这样的缓存,只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。



坚持原创技术分享,您的支持将鼓励我继续创作!



分享

评论