WeakHashMap源码分析与ThreadLocal的内存泄漏

涉及问题:

  • WeakHashMap和ThreadLocal的异同,两者分别怎样清理失效的Entry?
  • 为什么ThreadLocal会内存泄漏?原理类似的WeakHashMap会内存泄漏吗?

在网上查没看懂,以下是自己看源码后的一些思考,很口语化且不保证一定正确,欢迎纠错

WeakHashMap源码

在这里插入图片描述

WeakHashMap.Entry直接继承自WeakReference,是弱引用本身,它持有的referent指向对象A(key)。另外,WeakHashMap.Entry中持有强引用value

对象A不再被外部引用后会被回收,回收时WeakHashMap.Entry对象作为弱引用加入引用队列queue中

public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
	/**  
	 * The entries in this hash table extend WeakReference, using its main ref 
	 * field as the key. 
	 */
	private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {  
	    V value;  
	    final int hash;  
	    Entry<K,V> next;  
	  
	    /**  
	     * Creates new entry.     
	     */    
		Entry(Object key, V value,  
	          ReferenceQueue<Object> queue,  
	          int hash, Entry<K,V> next) {  
	        super(key, queue);  
	        this.value = value;  
	        this.hash  = hash;  
	        this.next  = next;  
	    }
	    // ...
	}
	// ...
}
public class WeakReference<T> extends Reference<T> {  
    public WeakReference(T referent, ReferenceQueue<? super T> q) {  
        super(referent, q);  
    }  
    // ...
}
public abstract class Reference<T> {  
    private T referent;         /* Treated specially by GC */  
    volatile ReferenceQueue<? super T> queue;
    // ...
}

expunge:清除
Stale:不新鲜的
Entries:条目(键值对)
失效条目指这样的条目:Entry(key=null, value=A)

get()、put()、size()等方法都会调用expungeStaleEntries()。expungeStaleEntries()会把队列中的所有引用取出。取出的Object x的实际类型是WeakHashMap.Entry,转换为Entry<K,V>类型后,可以从中取出强引用value,将value置null,并从Map中把该Entry去除(此处即使不让value=null,value指向的值对象应该也能被回收,因为Map不再持有该Entry -> Entry被回收 -> Entry中value曾指向的值对象被回收,只是得经过多次GC才行。所以手动value=null应该是为了使值对象更快回收?):

public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
    // 清除所有失效的条目
    private void expungeStaleEntries() {
        // 轮询引用队列,移除所有条目
        for (Object x; (x = queue.poll()) != null; ) {
            // 同步队列以保证线程安全
            synchronized (queue) {
                @SuppressWarnings("unchecked")  // 抑制类型转换警告
                Entry<K,V> e = (Entry<K,V>) x;  // 将Object转换为Entry类型
                
                int i = indexFor(e.hash, table.length);  // 计算哈希值对应的索引位置
                
                Entry<K,V> prev = table[i];  // 当前索引位置的头结点
                Entry<K,V> p = prev;  // 从头结点开始遍历链表
                
                // 遍历链表直到尾部
                while (p != null) {
                    Entry<K,V> next = p.next;
                    // 如果当前节点是失效的节点
                    if (p == e) {
                        // 如果是链表的第一个节点
                        if (prev == e) {
                            table[i] = next;
                        } else {
                            prev.next = next;
                        }
                        // 此处不能将e.next置为null,因为可能被迭代器使用
                        e.value = null;  // 清除值引用,帮助垃圾回收
                        size--; // 减少有效条目计数
                        break; 
                    }
                    // 移动到下一个节点继续查找
                    prev = p;
                    p = next;
                }
            }
        }
    }
    // ...
}
例子:WeakHashMap用法

需要经过几次GC才能看到值对象被回收(使用JDK8,参数:-XX:+PrintGCDetails -Xmn10m -Xmx10m ):

public class WeakHashMapExample {  
    public static void main(String[] args) {  
        WeakHashMap<String, Value> map = new WeakHashMap<>();  
        String key = new String("key");  
        // 注意:  
        // 此处不能使用字面值定义,否则key和字符串常量池中的引用指向同一对象,即使key=null该对象也不能被回收  
        // 而使用new定义时,key指向的对象和字符串常量池中的对象不是同一对象,key=null时前者可被回收  
//        String key = "key";  
  
        map.put(key, new Value());  
        System.out.println("weakHashMap: " + map);  
        key = null; // 这意味着"弱键"key1再没有被其它对象引用,调用gc时会回收WeakHashMap中与key1对应的键值对  
  
        // 内存回收,这里会回收WeakHashMap中与"key0"对应的键值对  
        System.out.println("GC 1:");  
        System.gc();  // 此次GC回收key字符串对象,同时使弱引用加入referenceQueue
        try {  
            Thread.sleep(100); // 等待GC  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        map.get("1"); // 调用get()方法触发expungeStaleEntries(),从referenceQueue取出弱引用把对应value置为null  
        System.out.println("GC 2:");  
        System.gc(); // value=null后,此次GC可以回收Value对象  
        try {  
            Thread.sleep(100);  
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }  
        System.out.println("GC 3:");  
        System.gc(); // Value对象被回收后,此次GC可以回收之前被Value对象持有引用的byte[]数组  
        // 打印WeakHashMap  
        System.out.println("weakHashMap: " + map);  
    }
}

在这里插入图片描述

ThreadLocal

原理类似WeakHashMap,只是ThreadLocal使用线性探测法而不是链地址法,另外也没有使用ReferenceQueue,所以它清理失效Entry没有WeakHashMap积极(几乎不会做对所有失效Entry的清理):

public class ThreadLocal<T> {
	// 虽然ThreadLocalMap类定义在ThreadLocal里,但ThreadLocalMap对象存在Thread里
	static class ThreadLocalMap {  
		// ThreadLocalMap.Entry继承自弱引用
		static class Entry extends WeakReference<ThreadLocal<?>> {  
	        /** The value associated with this ThreadLocal. */  
	        Object value;  
	        Entry(ThreadLocal<?> k, Object v) {  
	            super(k);  
	            value = v;  
	        }  
	    }
	    private Entry[] table;  
		private int size = 0;
		// ...
	}
	// ...
}
class Thread implements Runnable {
	// ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. 
	ThreadLocal.ThreadLocalMap threadLocals = null;
	// ...
}
set():

虽然set()中也调用了expungeStaleEntry(int staleSlot),但和WeakHashMap不一样,ThreadLocalMap没有使用引用队列,所以不具体知道哪些Entry需要清除。所以set()中并不会清除所有失效Entry,只会为了腾出空间而做消极的清除,另外把冲突链上的失效Entry顺带清除一下:

public class ThreadLocal<T> {
    // 设置当前线程局部变量的值
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 获取当前Thread对象的ThreadLocalMap
        if (map != null)
            map.set(this, value);  // 调用ThreadLocalMap的set()方法来存储键值对
        else
            createMap(t, value);  // 如果ThreadLocalMap为空,则创建一个新的Map并存储键值对
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;  // 返回与当前线程关联的ThreadLocalMap
    }
    
    static class ThreadLocalMap {
        // 内部类ThreadLocalMap,用于存储ThreadLocal的键值对
        private Entry[] table;  // ThreadLocalMap的哈希表,存储键值对
        private int size = 0;  // 哈希表中条目的数量
        
        // 存储键值对
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;  // ThreadLocalMap的哈希表
            int len = tab.length;  // 哈希表的长度
            int i = key.threadLocalHashCode & (len-1);  // 计算key的哈希值,并定位到哈希表的索引
            
            // 使用线性探测法寻找合适位置或冲突链中的key相等的条目
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {  // 如果找到key相等的条目
                    e.value = value;  // 直接更新value
                    return;
                }
                if (k == null) {  // 如果当前条目中的键已被回收
                    replaceStaleEntry(key, value, i);  // 清除失效条目,并用新键值对替换
                    return;
                }
                // 如果当前条目被占用且key不匹配,继续探测到下一个索引
            }
            
            // 如果没有找到key相等的条目,并且没有遇到null(即没有冲突链中的空位)
            tab[i] = new Entry(key, value);  // 在探测到的第一个null位置创建新的条目
            int sz = ++size;  // 增加条目数量
            
            // 清理一些失效的条目,并检查是否需要进行扩容操作
            if (!cleanSomeSlots(i, sz) && sz >= threshold)  // 清理并检查扩容阈值
                rehash();  // 如果需要,进行扩容操作
        }
        
        // 线性探测的辅助方法,获取下一个索引
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
        
        // 清理一些失效的条目
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;  // 标记是否清理了任何条目
            Entry[] tab = table;  // 当前哈希表
            int len = tab.length;  // 哈希表长度
            
            // 从当前索引开始,尝试清理失效的条目
            do {
                i = nextIndex(i, len);  // 移动到下一个索引
                Entry e = tab[i];
                
                if (e != null && e.get() == null) {  // 如果条目存在但键已被回收
                    n = len;  // 重置索引,重新开始清理
                    removed = true;  // 标记清理了条目
                    i = expungeStaleEntry(i);  // 清除失效条目
                }
            } while ( (n >>>= 1) != 0 );
            
            return removed;  // 返回是否清理了条目
        }
        
        // 清除索引为staleSlot的失效条目,并重新哈希冲突链上的其他条目
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;  // 当前线程的ThreadLocalMap的哈希表
            int len = tab.length;  // 哈希表的长度
            
            // 清除索引为staleSlot的条目
            tab[staleSlot].value = null;  // 设置值对象为null
            tab[staleSlot] = null;  // 设置条目本身为null
            size--;  // 减少ThreadLocalMap的大小
            
            // 从staleSlot的下一个索引开始,重新哈希直到遇到null
            Entry e;  // 当前条目
            int i;  // 当前索引
            
            for (i = nextIndex(staleSlot, len); (e = tab[i]) != null;  i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();  // 获取当前条目的键(ThreadLocal对象)
                
                if (k == null) {  // 如果键已经被回收
                    e.value = null;  // 清除值对象
                    tab[i] = null;  // 清除条目本身
                    size--;  // 减少ThreadLocalMap的大小
                } else {
                    // 计算键的哈希值,并与哈希表长度进行模运算,得到新的哈希位置
                    int h = k.threadLocalHashCode & (len - 1);
                    
                    if (h != i) {  // 如果当前条目的哈希位置与新的哈希位置不一致
                        tab[i] = null;  // 清除当前位置的条目
                        
                        // 扫描直到找到null位置,以便重新放置当前条目
                        // 与Knuth算法不同,我们需要扫描到null,因为可能有多个条目已经失效
                        while (tab[h] != null) {
                            h = nextIndex(h, len);  // 找不到null位置,继续扫描
                        }
                        tab[h] = e;  // 放置当前条目到新的哈希位置
                    }
                }
            }
            return i;  // 返回当前扫描的索引,此时索引指向null
        }
        
        // ...
    }
    
    // ...
}
get():

只会把冲突链上的失效Entry顺带清除一下:

public class ThreadLocal<T> {
    // 获取当前线程局部变量的值
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 获取当前Thread对象的ThreadLocalMap
        
        if (map != null) { 
            ThreadLocalMap.Entry e = map.getEntry(this);  // 获取键为当前ThreadLocal的条目
            if (e != null) {  // 如果条目存在
                @SuppressWarnings("unchecked")  // 抑制类型转换的警告
                T result = (T)e.value;  // 强制类型转换并返回值
                return result;
            }
        }
        
        // 如果没有找到条目,则初始化并设置初始值
        return setInitialValue();  
    }
    
    // ...
    static class ThreadLocalMap {
        // 根据键获取对应的条目
        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);  // 继续搜索冲突链
        }
        
        // 在首次获取条目未命中后,继续搜索冲突链
        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;  // 如果没有找到匹配的条目,返回null
        }
        
        // ...
    }
}
remove():

把key(ThreadLocal对象)对应的value置为null,以免值对象(本地线程中的数据副本)被一直存活的ThreadLocalMap对象持有,无法被回收。除当前key所在Entry外只会把冲突链上的失效Entry顺带清除一下

public class ThreadLocal<T> {
	public void remove() {  
	    ThreadLocalMap m = getMap(Thread.currentThread());  
	    if (m != null)  
	        m.remove(this);  
	}
	// ...
}
public class ThreadLocal<T> {
	static class ThreadLocalMap {  
		private void remove(ThreadLocal<?> key) {  
			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)]) {  
		        if (e.get() == key) {  
		            e.clear();  
		            expungeStaleEntry(i);  
		            return;        
				}  
		    }  
		}
		
		// 用于清除索引为staleSlot的失效条目,并重新哈希冲突链上的其他条目
		private int expungeStaleEntry(int staleSlot) {  
		    Entry[] tab = table;  // 获取当前线程的ThreadLocalMap的哈希表
		    int len = tab.length;  // 获取哈希表的长度
		  
		    // 清除索引为staleSlot的条目
		    tab[staleSlot].value = null;  // 设置值对象为null
		    tab[staleSlot] = null;  // 设置条目本身为null
		    size--;  // 减少ThreadLocalMap的大小
		  
		    // 从staleSlot的下一个索引开始,重新哈希直到遇到null
		    Entry e;  
		    int i;  
		    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null;  i = nextIndex(i, len)) {  
		        ThreadLocal<?> k = e.get();  // 获取当前条目的键(ThreadLocal对象)
		        
		        if (k == null) {  
		            // 如果键已经被回收,清除当前条目
		            e.value = null;  
		            tab[i] = null;  
		            size--;  
		        } else {  
		            // 计算键的哈希值,并与哈希表长度进行模运算,得到新的哈希位置
		            int h = k.threadLocalHashCode & (len - 1);  
		            
		            if (h != i) {  
		                // 如果当前条目的哈希位置与新的哈希位置不一致,需要移动条目
		                tab[i] = null;  
		                
		                // 扫描直到找到null位置,以便重新放置当前条目
		                // 与Knuth算法不同,我们需要扫描到null,因为可能有多个条目已经失效
		                while (tab[h] != null) {
		                    h = nextIndex(h, len);  // 找不到null位置,继续扫描
		                }
		                tab[h] = e;  // 放置当前条目到新的哈希位置
		            }
		        }
		    }
		    return i;  // 返回当前扫描的索引,此时索引指向null
		}
		// ...
	}
	// ...
}
expungeStaleEntries()

这个方法会遍历整个哈希表来清理所有失效Entry,但只在Re-pack或re-size哈希表时才会被调用(只会在set()中被调用):

public class ThreadLocal<T> {
	static class ThreadLocalMap {  
		private int size = 0;  // The number of entries in the table.
		private int threshold; // The next size value at which to resize.
		
		private void set(ThreadLocal<?> key, Object value) {  
		    // ... 
		    int sz = ++size;  
		    // size要超过阈值,调整哈希表大小,并重新哈希
		    if (!cleanSomeSlots(i, sz) && sz >= threshold)  
		        rehash();  // 只在这里被调用
		}
		
		// Re-pack and/or re-size the table. First scan the entire table removing stale entries. 
		// If this doesn't sufficiently shrink the size of the table, double the table size.
		private void rehash() {  
		    expungeStaleEntries();  
		    // Use lower threshold for doubling to avoid hysteresis  
		    if (size >= threshold - threshold / 4)  
		        resize();  
		}
		
		// Expunge all stale entries in the table.
		private void expungeStaleEntries() {  
		    Entry[] tab = table;  
		    int len = tab.length;  
		    for (int j = 0; j < len; j++) {  
		        Entry e = tab[j];  
		        if (e != null && e.get() == null)  
		            expungeStaleEntry(j);  
		    }  
		}
		// ...
	}
	// ...
}
HashMap实现的区别

WeakHashMap使用链地址法,ThreadLocal使用线性探测法

  • 链地址法:如果计算出的索引位置已经有其他条目,那么新的条目不会替换掉原有条目,而是在该索引位置形成一个链表。新条目被添加到链表的头部或尾部(取决于具体的实现)
  • 线性探测法:如果计算出的索引位置已经有其他条目,那从当前位置开始,按照固定顺序(通常是线性顺序)探测下一个位置,直到找到一个空闲的位置。将新条目插入探测到的第一个空闲位置
内存泄漏问题

为什么不一样呢?

  1. ThreadLocal:首先,要想在key(ThreadLocal对象)被回收后再找到其对应的value,就只能去遍历ThreadLocalMap(实际上ThreadLocalMap的expungeStaleEntries()就是靠遍历来找到所有失效Entry的),但是每次回收都去通过遍历来清除明显是不现实的,而且ThreadLocalMap的方法也是不暴露给外部的,所以我们只能选择在key被回收前通过它把对应的value先行置null,即,赶在key被回收前手动调用key.remove()。WeakHashMap是使用了引用队列,才能准确得知哪个Entry需要清理,既不需要依靠key来找Entry,也能实现在map.get()、put()、size()等各种操作后都顺带清理一次所有的失效Entry
  2. ThreadLocal不调key.remove()等方法就会内存泄漏的主要原因其实是:ThreadLocalMap本身很难被回收,因为它被所在线程thread对象持有。一旦没有手动清除Entry(key=null, value=A),对象A就会存活到线程结束
  3. 另外key.remove()、get()、set()等都并不能保证把map里的所有失效Entry清理掉,比如key.remove()只能保证把key对应的Entry清理掉
  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值