ThreadLocal内存泄漏问题分析

什么是内存泄漏

简单来讲,内存泄漏就是JVM垃圾回收器对某个对象占据的内存在较长时间内一直没法回收,没法回收的原因并不是因为垃圾回收器有bug,而是由于对象没法判定为垃圾(但实际上该对象已经是不会被使用了)。
这里说的“较长时间”是一个相对的概念,没有固定的时间规定,内存泄漏侧重点在于对象不需要了而又没法回收。

ThreadLocal的存储原理

ThreadLocal存储在Thread的ThreadLocalMap对象里面,ThreadLocalMap名字带了Map但实际跟我们常说的Map并不一样,只是功能上和我们常说的Map相似。
通过查看ThreadLocal的get、set方法源码可以发现,我们在get、set时都是先获取到ThreadLocalMap对象,然后再进行取值或是赋值操作。

public class ThreadLocal<T> {
	...
	public T get() {
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
	}

	public void set(T value) {
	    Thread t = Thread.currentThread();
	    ThreadLocalMap map = getMap(t);
	    if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
	}

	ThreadLocalMap getMap(Thread t) {
		return t.threadLocals;
	}

	static class ThreadLocalMap {
		//这里可以看出是没有实现Map接口的,和我们常用的HashMap不一样,ThreadLocalMap是ThreadLocal的一个内部类
		...
	}
	...
}

public class Thread implements Runnable {
	...
	//每个线程实例都有一个threadLocals属性
	ThreadLocal.ThreadLocalMap threadLocals = null;
	...
}

ThreadLocalMap.Entry

ThreadLocalMap使用一个Entry数组来存放数据,Entry为ThreadLocalMap的内部类,虽然和Map.Entry同名,但也不是同一个类。ThreadLocalMap.Entry继承了弱引用类WeakReference,使用一个ThreadLocal变量作为弱引用的referent(也就是很多人常说的key),另外还有一个属性value。

public class ThreadLocal<T> {
	...
	static class ThreadLocalMap {
		...
		private Entry[] table;

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

			//继承WeakReference必须有一个带参的构造方法,当然参数不用跟WeakReference类完全一样
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		...
	}
	...
}

ThreadLocal工作流程分析

一般我们会在某个对象里面定义一个ThreadLocal的成员变量,这样就能在这个对象的所有方法里面使用到了。假如定义为某个方法的局部变量,那可使用范围就比较窄了。

@Service
public class ThreadLocalServiceImpl implements IThreadLocalService {
	
	private ThreadLocal<String> localVariable = new ThreadLocal<>();

	@Override
    public String getVariable() {
    	localVariable.set("1111");
		...
		//中间省略了一些复杂的业务方法调用,可能业务方法里面本身也有对localVariable进行了处理
		System.out.println(localVariable.get());
        return localVariable.get();
    }
}

1.当我们定义了一个localVariable变量时,ThreadLocalServiceImpl就对localVariable有了一个强引用关系。此时还未与ThreadLocalMap产生任何关系;
2.紧接着,我们调用localVariable的set方法,此时当前线程的ThreadLocalMap就会新生成一个Entry对象,key(或者说referent)就是localVariable对象,value为我们set的值,Entry对localVariable是一个弱引用关系,但是Entry的value是强引用;
3.再然后我们打印了set的值,然后返回了,service方法执行结束。
在上述service方法执行结束之后,ThreadLocalServiceImpl对localVariable的强引用就终止了,但是当前线程没有结束,这里假设到了另外一个service里,所以Entry对localVariable的弱引用还是存在的,由于是弱引用,触发GC时localVariable就会被回收掉,此时Entry的状态就是key为null,value还有值,value无法被使用也无法被回收,内存泄漏产生。

为什么使用弱引用

假设一个不懂ThreadLocal原理的人来按上述示例代码实现功能时,如果没有弱引用(意思是假设Entry对localVariable是一个强引用关系),那最终的结果就是不仅value没被回收,key也不会被回收。因为直观感觉是localVariable为ThreadLocalServiceImpl成员变量,ThreadLocalServiceImpl的方法都执行完了,这个变量应该也被回收了。
所以弱引用是一定程度上帮助我们减轻了内存泄漏问题。另外ThreadLocal的get、set、remove方法本身有清除掉key为null的Entry的功能,所以从设计上来说也是合理的:在不出现问题情况下短时间、少量的内存溢出是合理的。

怎么避免value内存泄漏的问题

使用完之后要及时调用remove方法,这样可以直接移除掉key并触发清除key为null的Entry的功能。

@Service
public class ThreadLocalServiceImpl implements IThreadLocalService {
	
	private ThreadLocal<String> localVariable = new ThreadLocal<>();

	@Override
    public String getVariable() {
    	localVariable.set("1111");
		...
		//中间省略了一些复杂的业务方法调用,可能业务方法里面本身也有对localVariable进行了处理
		System.out.println(localVariable.get());
		String res = localVariable.get();
		localVariable.remove();
        return res;
    }
}

总结

ThreadLocal内存泄漏的根本原因是ThreadLocalMap存在于线程,生命周期比较长,弱引用是用来减轻内存泄漏问题的,并不是设计缺陷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值