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;
<think>我们正在讨论WeakHashMap如何防止内存泄漏的原理及实现方式。根据提供的引用材料,WeakHashMap利用弱引用机制实现自动清理,从而避免内存泄漏。原理:1.WeakHashMap的键(Key)是通过弱引用(WeakReference)保存的。这意味着,当除了WeakHashMap自身之外,没有其他强引用指向该键对象时,该键对象就可以被垃圾回收器回收。2.一旦键对象被回收,WeakHashMap会检测到这一点,并在后续操作(如调用get、put、size等方法)时自动移除对应的条目(键值对)。这样,值对象也就变得可以被回收(前提是没有其他强引用指向它)。实现方式:1.WeakHashMap内部使用一个引用队列(ReferenceQueue)来跟踪被回收的键。当弱引用指向的键被垃圾回收时,这个弱引用会被加入到引用队列中。2.WeakHashMap在每次执行操作(如get、put、size等)时,会先检查引用队列,将队列中已经回收的键对应的条目从Map中移除。这个过程称为“清理(expunge)”操作。引用材料中的示例代码:WeakHashMap<Object,String>weakMap=newWeakHashMap<>();Objectkey=newObject();weakMap.put(key,"重要数据");key=null;//断开强引用System.gc();//建议GC执行(仅演示用)//此时,由于没有其他强引用指向key,它会被回收,weakMap中的条目也会被清除HashMap的区别:HashMap的键是强引用,即使键已经不再使用(即没有其他有效引用指向它),只要Map还存在,键对象就不会被回收,从而导致内存泄漏。而WeakHashMap的键是弱引用,不会阻止垃圾回收。适用场景:适合用作临时缓存,例如存储一些可以被重建的数据,当内存不足时,这些数据可以被自动释放。注意:值对象是通过强引用保存在WeakHashMap中的,因此,如果值对象直接或间接引用了键对象,那么即使键的强引用被置为null,由于值对象的存在,键对象也不会被回收,这会导致内存泄漏。因此,使用WeakHashMap时,值对象不应该持有键对象的强引用。引用材料[4]中的示例也说明了这一点:当外部强引用消失后,WeakHashMap中的条目会被回收,而HashMap中的则不会。引用材料[2]和[3]也强调了WeakHashMap作为缓存时自动释放的特性。因此,WeakHashMap防止内存泄漏的原理就是通过弱引用管理键,并在键被回收后自动清理对应的条目。下面我们用代码示例和原理图来进一步说明:代码示例(来自引用[1]):WeakHashMap<Object,String>weakMap=newWeakHashMap<>();Objectkey=newObject();weakMap.put(key,"重要数据");System.out.println(weakMap.containsKey(key));//truekey=null;//断开强引用System.gc();//建议GC执行(仅演示用)System.out.println(weakMap.isEmpty());//很可能输出true!原理图(简化):-创建键对象:Objectkey=newObject();此时,key是一个强引用,指向堆中的对象。-将键值对放入WeakHashMap:weakMap.put(key,"重要数据");此时,WeakHashMap内部创建一个弱引用(WeakReference)指向key对象,并将该弱引用值关联。-断开强引用:key=null;此时,只有弱引用指向该键对象。-触发垃圾回收:System.gc();垃圾回收器会回收该键对象(因为没有强引用了)。-弱引用被放入引用队列(ReferenceQueue),WeakHashMap在后续操作中会从引用队列中取出这个弱引用,并移除对应的条目。总结:WeakHashMap通过弱引用和引用队列机制,实现了在键对象不再被外部使用时自动清理条目的功能,从而避免了因Map长期持有对象而导致的内存泄漏。相关问题:1.WeakHashMap在什么情况下不会自动清理条目?2.如何正确使用WeakHashMap作为缓存?3.除了WeakHashMap,还有哪些方式可以避免缓存引起的内存泄漏?</think>### WeakHashMap 防止内存泄漏的原理及实现方式 #### 一、核心原理:弱引用机制 WeakHashMap 防止内存泄漏的核心在于其**键(Key)使用弱引用(WeakReference)** 存储,而非普通 HashMap 的强引用。这种设计使得当键对象失去外部强引用时,可被垃圾回收器(GC)自动回收,进而触发条目自动清理。 ```java WeakHashMap<Object, String> weakMap = new WeakHashMap<>(); Object key = new Object(); weakMap.put(key, "重要数据"); // key 被弱引用持有 key = null; // 断开强引用 System.gc(); // GC 触发回收 System.out.println(weakMap.isEmpty()); // 输出 true (条目被自动移除)[^1][^4] ``` #### 二、实现机制详解 1. **弱引用键的存储结构** WeakHashMap 内部通过 `Entry<K,V> extends WeakReference<K>` 实现: ```java private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> { V value; Entry(K key, V value, ReferenceQueue<K> queue) { super(key, queue); // 关键:弱引用绑定引用队列 this.value = value; } } ``` 2. **引用队列(ReferenceQueue)协作** - 当键对象被 GC 回收时,其弱引用会被自动加入引用队列 - WeakHashMap 在执行操作时(如 `get`/`put`/`size`)自动轮询队列: ```java private void expungeStaleEntries() { Reference<? extends K> ref; while ((ref = queue.poll()) != null) { // 从 Map 中移除被回收键对应的条目 removeEntry(ref); } } ``` 3. **值对象的释放** 虽然值对象(Value)仍通过强引用存储,但由于键被移除: ```mermaid graph LR A[外部强引用消失] --> B[键对象被GC回收] B --> C[弱引用入队] C --> D[WeakHashMap移除条目] D --> E[值对象失去关联] E --> F[值对象可被GC回收] ``` #### 三、传统 HashMap 的关键差异 | 特性 | HashMap | WeakHashMap | |------------------|-----------------------|----------------------| | **键引用类型** | 强引用 | 弱引用 | | **GC 行为** | 阻止键被回收 | 允许键被回收 | | **内存泄漏风险** | 高(需手动移除) | 低(自动清理) | | **适用场景** | 长期缓存 | 临时缓存/监听器 | #### 四、典型应用场景 1. **临时缓存系统** ```java public class TempCache { private WeakHashMap<String, Object> cache = new WeakHashMap<>(); public void put(String key, Object value) { cache.put(key, value); // 弱引用存储 } public Object get(String key) { return cache.get(key); // 若键被回收则返回 null[^2] } } ``` 2. **监听器管理** 避免因未注销监听器导致内存泄漏: ```java WeakHashMap<Listener, Boolean> listeners = new WeakHashMap<>(); button.addListener(new Listener() { // 监听器对象将被自动回收 }); ``` 3. **内存泄漏检测工具** 类似 LeakCanary 原理:用弱引用跟踪 Activity,通过引用队列判断是否泄漏[^5]。 #### 五、使用注意事项 1. **值对象风险** 值对象若直接引用键对象,会形成循环引用阻止回收: ```java // 错误示例:值对象强引用键 weakMap.put(key, key); ``` ✅ 解决方案:确保值不持有键的强引用 2. **非线程安全** WeakHashMap 非线程安全,多线程环境需用 `Collections.synchronizedMap` 包装。 3. **性能影响** 自动清理机制会在操作时增加开销,高频访问场景需评估性能。 #### 六、其他引用类型对比 | 引用类型 | GC 回收条件 | 典型应用 | |----------|---------------------|------------------------| | 强引用 | 永不回收 | 核心业务对象 | | 软引用 | 内存不足时回收 | 图片缓存 | | 弱引用 | 下次 GC 即回收 | WeakHashMap / 监听器 | | 虚引用 | 对象回收时收到通知 | 资源清理监控 | > **核心结论**:WeakHashMap 通过弱引用键 + 引用队列协作机制,实现当键对象失去外部强引用时自动清理条目。这种设计使其成为防止因未释放缓存/监听器导致内存泄漏的理想工具,尤其适用于临时性数据存储场景[^1][^3]。 --- ### 相关问题 1. **WeakHashMap 在什么场景下仍可能导致内存泄漏?如何避免?** 2. **如何实现一个基于软引用(SoftReference)的缓存系统? WeakHashMap 有何差异?** 3. **除了引用队列,还有哪些 JVM内存泄漏检测技术?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值