ThreadLocal内存泄漏问题分析与java中的强引用,软引用,弱引用,虚引用四种引用类型

1、java中的四种引用类型

     强引用,软引用,弱引用,虚引用

1.1 强引用

     强引用是我们常见的引用,创建完对象后,根节点不可达时,被垃圾回收期回收。

1.2 软引用

   Java中使用SoftReference<> 作为软引用工具,范型中的引用则为软引用。如下面代码:

SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);
System.out.println(m.get());

对上述代码,m指向SoftReference对象,SoftReference对象指向byte数组即为软引用,如下图:

软引用有什么用呢?

软引用对象在堆内存不足时,会被垃圾回收。堆内存足够多时不会回收软引用。

因此软引用适合做缓存。

 

1.3 弱引用

  与软引用类似,弱引用使用WeakReference作为弱引用工具,如下代码:

WeakReference<M> m = new WeakReference<>(new M());
System.out.println(m.get());

WeakReference对象m的引用 new M();即为弱引用。

弱引用只要有垃圾回收,而且没有强引用指向的时候,就会立即被回收。

弱引用是为了解决某些地方内存泄漏的问题,如:用在ThreadLocal中。

 

1.4 虚引用

   虚引用,使用phantomReference作为引用工具,不同的是构造函数多了一个参数QUEUE,如下代码。

private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

public static void main(String[] args) {

    PhantomReference<M> phantomReference = new PhantomReference<>(new M(),QUEUE);
    System.out.println(phantomReference.get());

}

代码中phantomReference.get()是永远拿不到值的,当内存被回收的时候,会把信息发到QUEUE队列中。这样监听者就可以通过队列中的消息判断虚引用被回收了。

那么虚引用的作用是什么呢?

虚引用是用来管理堆外内从。

如NIO引用了零拷贝概念,内从不会拷贝到堆内存,而是在堆外。jvm引用虚引用,如DirectByteBuffer指向堆外内存,就有一个虚引用来管理这块堆外内存,当虚引用被回收时,垃圾回收器判断堆外内存该回收了。

 

2、上面介绍了java的4中引用类型,只有弱引用没有图示,这要结合ThreadLocal来分析理解。

ThreadLocal是线程的本地存储,通过线程中的ThreadLocalMap来存储当前线程的信息,同时与其他线程隔离开来,互不影响。

而ThreadLocalMap的基本元素Entry就是一个弱引用,我们看看源码。

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

我们知道ThreadLocalMap的key值为ThreadLocal对象,通过上面代码我们发现key是被弱引用对象指向的引用,代码中super(k);

那么这个key就为弱引用,在每次垃圾回收时都会被回收掉。

那么就带出了一个问题,ThreadLocalMap中的key为弱引用,但是value为强引用,当GC发生后,key变成了null,但是value一直存在,并且仍然是根节点可达的对象,无法被垃圾回收,这样就会产生内从泄露了。

如下图:

弱引用只要有垃圾回收,而且没有强引用指向的时候,就会立即被回收。

 

3、ThreadLocal内存泄露问题解决

JDK源码设计的时候已经考虑过这个问题,ThreadLocal的set和get方法通过ThreadLocalMap做了对旧值的擦除操作。

ThreadLocalMap.set方法:每一次set新值的时候,首先根据threadLocal的hashCode计算在Entry数组中的位置,然后向后环形对entry进行以下操作:

如果entry为null,则直接插入,然后调用cleanSomeSlots方法检测并清除旧entry;
如果entry的k等于当前的threadLocal,代表是同一个threadLocal,直接替换value;
如果当前的entry不为null但k为null,代表上述内存泄漏的情况,调用replaceStaleEntry处理旧entry并set新值;
如果entry不为null,entry的k不为null也不等于当前的threadLocal,代表hash冲突,可能是不同的threadLocal计算出相同的位置,已经有其他threadLocal在这个位置了,然后向后环形重复上述操作;
     

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap.getEntry方法:每一次get的时候,首先根据threadLocal的hashCode计算在Entry数组中的位置,然后进行以下操作:

如果entry不为null,并且entry中的k等于当前threadLocal,返回entry中存储的value;

如果entry为null,或者entry的k不等于当前threadLocal,查看getEntryAfterMiss方法,对entry数组向后环形遍历;

当entry不为null且entry的k等于当前threadLocal,返回entry里的value;

当entry不为null但entry的k为null,处理当前entry及其value防止内存泄漏;

当entry既不等于当前threadLocal也不为null,说明hash冲突,向后查找下一个entry继续上述操作;
     

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

问题2:如果两个threadLocal产生hash冲突,按相邻位置set,比如[0]和[1],如果第一个已经垃圾回收,按上述条件会调用expungeStaleEntry,第二个threadLocal处于正常状态是怎么取到值的?

答:关键就在于expungeStaleEntry,如代码所示,首先会将第一个threadLocal所属entry清理掉,然后把相邻threadLocal的entry改到这个位置,即由[1]改到[0],并将[1]上的entry置为null。

     

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值