ThreadLocal怎么才能导致内存溢出

文章详细介绍了JDK中ThreadLocal从早期版本到JDK8的内部结构变化,如何通过弱引用来避免内存泄漏,以及一个可能导致内存溢出的代码示例。强调了在使用ThreadLocal时应避免内存泄漏,特别是在线程池复用的情景下,用完后应及时移除不再使用的ThreadLocal实例。
摘要由CSDN通过智能技术生成

在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal实例,每一个ThreadLocal实例拥有一个Map实例。

在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例拥有一个Map实例。另外,Map结构的Key值也发生了变化:新的Key为ThreadLocal实例。这样做有如下的好处:

  • 当前的java应用在多核大内存的机器上,成千上万的线程很常见,而且大部分都是分布式应用,ThreadLocal变量使用的并不多,一般也就用于计算调用链耗时、登录鉴权等场景,旧的实现是一个map中有大量的元素(因为用Thread做key),新的实现是每个Thread有一个Map,元素很少。ThreadLocalMap是一个Map接口的简单实现,元素越少越容易处理,比如hash冲突、扩容等等
  • 线程终止后,线程相关的都会被回收,新的实现可以节约内存

然后有一个问题,ThreadLocalMap中放的entry,key为threadlocal实例,value是线程自己的值,每个线程都有自己的map,这里entry是一个弱引用,指向ThreadLocal实例,如果ThreadLocal实例没有强引用,只有一个弱引用指向它,那么垃圾回收线程就会回收这个ThreadLocal实例,这时候用entry.get()就返回null,也就可以回收对应的value了。

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

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

用弱引用的好处是如果ThreadLocal实例如果没有强引用,只有弱引用,那么垃圾回收就会回收它,但是如果线程活着,垃圾回收器不会回收Entry,Value也会一直存在。那这里用弱引用其实就是为了做清理,相当于一个兜底,但是这个兜底还依赖对ThreadLocal对象 set() get() remove()方法的调用,可以是其他ThreadLocal对象,清理的是Thread拥有的map,如果不调用,就没有清理动作。可以看一下ThreadLocal#expungeStaleEntry()。

下面是一个可以导致内存溢出的代码:JVM参数: -Xmx64M -Xms32M,因为64位jvm默认开启指针压缩,因此大概在大于1500个线程之后就会OutOfMemory。

    //如果threadlocal是局部变量,只get,new很多线程,能不能导致内存泄露呢?
    public static void main(String[] args){
        //持有线程对象,防止执行完之后线程被回收,threadlocalmap也会被回收
        List<Thread> list = new ArrayList<>();
        //1500就不会溢出,1600就会,40KB*1600 = 64MB,jvm运行参数-Xmx64M
        for(int i = 0;i<1600;i++){
            Thread tt = new Thread(() -> {
                ThreadLocal<List<String>> localThreadLocal = new ThreadLocal<List<String>>(){
                    @Override
                    protected List<String> initialValue() {
                        //由于默认开启指针压缩,这里会占用40KB
                        return new ArrayList<>(10000);
                    }
                };

                List<String> aa = localThreadLocal.get();
                aa.add(0,"1");
                try {
                    //防止线程过早终止
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                }
            });
            list.add(tt);
            tt.start();
        }

        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            //throw new RuntimeException(e);
        }
        System.out.println(list.size());
    }

运行输出:

Exception in thread "Thread-1532" Exception in thread "Thread-1535" Exception in thread "Thread-1534" java.lang.OutOfMemoryError: Java heap space
	at java.util.ArrayList.<init>(ArrayList.java:153)
	at ThreadLocalTest$2.initialValue(ThreadLocalTest.java:62)
	at ThreadLocalTest$2.initialValue(ThreadLocalTest.java:58)
	at java.lang.ThreadLocal.setInitialValue(ThreadLocal.java:180)
	at java.lang.ThreadLocal.get(ThreadLocal.java:170)
	at ThreadLocalTest.lambda$main$1(ThreadLocalTest.java:66)
	at ThreadLocalTest$$Lambda$1/1747585824.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-1540" Exception in thread "Thread-1544" java.lang.OutOfMemoryError: Java heap space
结论
  • 一般情况下,ThreadLocal都是private static final,用完之后一定remove,正常使用肯定不会内存泄露,如果没有remove,在线程池复用的情况下,可能会出很难排查的线上问题,因此要养成好习惯。
  • 想要内存泄露,第一要大量线程长时间运行,第二就是ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他 ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉 ThreadLocal弱引用为null的Entry。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值