Java中的四种引用

前言

最近又重新看了下ThreadLocal,ThreadLocal源码中ThreadLocalMap内部类的Entry中的key是ThreadLocal类型,并且是弱引用。

    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

那就顺便再回顾下Java中的四种引用。

java语言提供了4种引用类型:强引用、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference),与引用密切相关的,还有一个引用队列ReferenceQueue。引用和引用队列的关系,对于垃圾回收来说非常重要。

Java中的四种引用

  • 强引用

    Object obj = new Object();

    这里的obj引用便是一个强引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

  • 软引用

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

  • 弱引用

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

  • 虚引用

    虚引用主要用来跟踪对象被垃圾回收器回收的活动。它允许你知道具体何时其引用的对象从内存中移除。 深入理解JAVA虚拟机一书中有这样一句描述:**“为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知”。**所以虚引用更多的是用于对象回收的监听:

    1. 重要对象回收监听 进行日志统计

    2. 系统gc监听 因为虚引用每次GC都会被回收,那么我们就可以通过虚引用来判断gc的频率,如果频率过大,内存使用可能存在问题,才导致了系统gc频繁调

    3. 用于实现细粒度的内存控制。比如有一个缓存的设计,程序修需要在确认原来的对象要被回收之后, 才申请内存创建新的缓存。

    4. 可以用于清除堆外内存,实现了一个回调机制,以后讲到NIO的时候会讲到。

引用和引用队列提供了一种通知机制,允许我们知道对象已经被销毁或者即将被销毁。软引用和弱引用差别不大,JVM一旦探测到对象只有弱引用或弱引用,首先clear WeakReference(SoftReference)的referent(如果对象没有重写finalize()方法,值referent为null,如果重写了,设置为finalizable,对于finalize()方法,后续会分析),然后被插入到ReferenceQueue;而虚引用则不同,对于虚引用,只有当对象被GC销毁时,才会加入到ReferenceQueue。

使用PhantomReference更好的控制一些关于对象生命周期的事情,当WeakReference放入ReferenceQueue时,并不能保证该referent是被销毁了。别忘了对象可以在finalize方法里再生。而使用PhantomReference,当在ReferenceQueue中发现PhantomReference时,可以保证referent已经被销毁了。

弱引用和软引用的实际应用

前面介绍了4种引用的概念,那么接下来就来看看实际的应用,学以致用才是王道。

SoftReference和WeakReference都经常用来作为缓存来使用。

对于WeakReference

  • 在Tomcat中有使用它来做缓存。
public final class ConcurrentCache<K,V> {

    private final int size;
	//新生代map
    private final Map<K,V> eden;
	//永久代map
    private final Map<K,V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

Tomcat中使用WeakReference实现了缓存,这个类的设计很有意思,使用一个ConcurrentHashMap类型的eden属性和一个WeakhashMap类型的longterm属性来存储数据,映射了JVM中的新生代和永久代。

向缓存中添加数据的方法

  public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }

如果当新生代map已经满了,将新生代的数据全部拷贝到永久代中,并且清掉新生代map中的内容,然后再将数据添加到新生代map中。

如果新生代map没有满,那么直接添加到新生代map中。我们发现,不管什么情况下,当前添加的数据是一定会被放入到新生代缓存中的,是不会被GC回收的。但是那些老的缓存数据就不一定有那么幸运了,他们被放入到了永久代map中,是可能被GC回收的。

获取缓存数据的方法

   public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

先去新生代拿,如果拿不到,查看是不是进入了永久代,如果永久代中有数据,那么就将数据重新放入到新生代中,因为这里的永久代map与JVM中的永久代不一样,这里的永久代map是使用WeakHashMap(key是弱引用类型)实现的,永久代map中的数据可能会被GC回收,所以要重新放入到新生代map中,防止被回收。所以当前被取的数据,只要没被GC回收掉,我们都要让他以一个强引用存在,防止被GC回收掉。

对于ConcurrentCache,当我们缓存的数据量大于size时,就会将原有的缓存数据放入到WeakhashMap中,这些数据就存在被清除的可能。

  • Mybatis中有使用WeakReference来实现缓存
public class WeakCache implements Cache {
  private final LinkedList<Object> hardLinksToAvoidGarbageCollection;
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  private final Cache delegate;
  private int numberOfHardLinks;

  public WeakCache(Cache delegate) {
      //代理Cache,最原始的PerpetualCache(使用HashMap存储缓存数据)
    this.delegate = delegate;
     //保存强引用的最大值
    this.numberOfHardLinks = 256;
      //强引用集合,防止被GC
    this.hardLinksToAvoidGarbageCollection = new LinkedList<Object>();
      //引用队列,用户回收无用的WeakEntry
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<Object>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    removeGarbageCollectedItems();
    return delegate.getSize();
  }

  public void setSize(int size) {
    this.numberOfHardLinks = size;
  }

  @Override
  public void putObject(Object key, Object value) {
      //存放缓存数据
      //先清理掉需要清除的无用WeakEntry
    removeGarbageCollectedItems();
      //然后构造一个WeakEntry,存入底层的HashMap
    delegate.putObject(key, new WeakEntry(key, value, queueOfGarbageCollectedEntries));
  }

  @Override
  public Object getObject(Object key) {
      //获取数据
      
    Object result = null;
    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
    WeakReference<Object> weakReference = (WeakReference<Object>) delegate.getObject(key);
      //如果数据还存在HashMap中,没有被彻底的清除
    if (weakReference != null) {
        //获取弱引用value
      result = weakReference.get();

      if (result == null) {
          //如果弱引用已经被清掉了,那么就直接彻底清除掉这条数据,防止内存泄漏
        delegate.removeObject(key);
      } else {
          //如果弱引用没有被清掉,我们要的数据还在,这个时候我们就需要想方设法的保证这条数据不被GC(我们总是认为最近被访问过的数据再今后是最有可能被再次访问的),
          //怎么保证呢,那就是让一个强引用指向他
          //添加到这个链表集合的首位
        hardLinksToAvoidGarbageCollection.addFirst(result);
          //这个防止GC的强引用集合是有大小限制的,如果超过了限制了,那么就只能让最早进入这个集合的元素走人了。这里的设计充分体现了一个原则,我们总是认为最近被访问过的数据再今后是最有可能被再次访问的
        if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
          hardLinksToAvoidGarbageCollection.removeLast();
        }
      }
    }
    return result;
  }

  @Override
  public Object removeObject(Object key) {
    removeGarbageCollectedItems();
    return delegate.removeObject(key);
  }

  @Override
  public void clear() {
    hardLinksToAvoidGarbageCollection.clear();
    removeGarbageCollectedItems();
    delegate.clear();
  }

  public ReadWriteLock getReadWriteLock() {
    return null;
  }
//清除掉无用的WeakEntry
  private void removeGarbageCollectedItems() {
    WeakEntry sv;
      //如果value 弱引用被回收了,那么就会加入到引用队列queueOfGarbageCollectedEntries中,该方法就是通过遍历引用队列,拿到每一个被回收的弱引用的key,然后彻底清除掉这条数据,防止内存泄漏
    while ((sv = (WeakEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);
    }
  }

    //与ThreadLocal内部类THreadLocalMap中的Entry以及WeakhashMap中的Entry不一样,这里是value为弱引用
    //底层的存储是HashMap,这里是将上层的value包装为WeakEntry,最后当成HashMap中的value存入HashMap,key依旧是上层传递过来的key
  private static class WeakEntry extends WeakReference<Object> {
    private final Object key;

    private WeakEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
      super(value, garbageCollectionQueue);
      this.key = key;
    }
  }

}

我们知道Mybatis的二级缓存是使用装饰者模式来实现的。默认是使用LruCache来做缓存。但是也提供了其他的缓存策略,包括FIFOCache,以及使用弱引用和软引用实现的WeakCache和SoftCache。

对于SoftReference

  • Mybatis中有使用SoftReference来实现缓存

    具体源码和WeakCache答大体一致,只是把WeakEntry换成SoftWeak,思路是一样的,感兴趣的可以查看源码细细品味。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值