ThreadLocal内存泄漏的原因

一、前言

在分析ThreadLocal导致的内存泄露前,需要了解一下内存泄露、强引用与弱引用以及GC回收机制。这样才能更好分析ThreadLocal泄漏的原因。

如果了解上述问题可直接跳到第二节。

1.1 内存泄漏

如果不会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。如果泄漏的数据量足够大,可能会引起内存溢出,导致程序异常结束。

1.2 强引用与弱引用

  • 强引用: 如 String name = new String(); 一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。可将 name = null,让JVM在合适的时候回收。
  • 弱引用: JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

1.3 GC是怎样判断对象能否被回收

  • 引用计数:对象被引用的时候,计数器加1,当计数器为0的时候代表对象可以被回收。
  • 可达性分析:从GC Roots向下搜索,也就是从根对象往下搜索,经过的地方为引用链,如果对象不在引用链,则代表可被回收。

二、ThreadLocal内存泄漏分析

2.1 ThreadLocal实现原理

查看ThreadLocal的set(T  value)方法,我们可以发现数据是存在了ThreadLocalMap的静态内部类Entry里面,其中key为使用弱引用的ThreadLocal实例,value为set传入的值。

代码如下

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
      map.set(this, value);
    else
      // 第一次调用的时候,将当前线程和value往下传。
      createMap(t, value);
}

void createMap(Thread t, T firstValue) {
   // 当前线程内部的变量 ThreadLocal.ThreadLocalMap threadLocals 设置为新建出来的对象。
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
      
      Object value;

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

2.2 ThreadLocal 内存泄漏的原因

引用图如下,实心箭头表示强引用,虚线箭头表示弱引用。

ThreadLocal内存泄漏的原因


从图中可以看出,当前线程强引用了ThreadLocalMap,ThreadLocal为ThreadLocalMap的弱引用Key,结合前言的1.2节,如果ThreadLocal没有被外部强引用,当系统触发GC时,会将ThreadLocal对象回收掉,会导致ThreadLocalMap的Key为null,value还是被当前线程强引用,只有当Thread线程退出后,value的强引用链才会断开。

如果线程不结束,则引用链一直存在,永远不会被回收,造成内存泄漏。

Thread -> ThreadLocalMap -> Entry -> value

2.2.1 代码演示内存泄漏

关键代码见注解

public class TestThreadLocal {
    public static void main(String[] args) {
        A a = new A();
        a.getLocal(); //获取ThreadLocal后不引用它,让GC能够回收弱引用对象。
        System.gc();
        Thread thread = Thread.currentThread(); // 断点打在此处。
    }
}

class A{
    public ThreadLocal<String> getLocal(){
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("zs");
        return threadLocal;
    }
}

debug运行代码,查看当前线程的ThreadLocalMap中的数据,可以发现引用的Key已经被GC回收了,造成了内存泄漏。

ThreadLocal内存泄漏的原因


2.3 key为什么使用弱引用

即使没有手动删除key和value,ThreadLocal在没有被引用的时候也会被回收。即ThreadLocalMap的key为null,下一次ThreadLocalMap调用set()、set()、remove()方法的时候会清除没被回收的value。

2.4 代码演示清除没被回收的value

 public static void main(String[] args) {
     ThreadLocal<String> local = new ThreadLocal<>();
     local.set("zs");
     // 模拟local没被引用,触发System.gc(),将key回收,设为null
     //将此处的thread的ThreadLocalMap的value为【zs】的key设为null
     Thread thread = Thread.currentThread();
     local.get();
     thread = Thread.currentThread();
     System.out.println(thread);
 }

deBug操作如下

ThreadLocal内存泄漏的原因

调用get方法的时候,由于map中的value对应的key为null,通过当前ThreadLocal对象去获取是获取value是获取不到Entry,于是调用初始化value的方法,清除原来的value。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) { // 为null跳出判断
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue(); // 调用初始化value的方法,清除原来的value
}

三、ThreadLocal的正确使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据。
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

四、总结

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。


原文始发于微信公众号(程序员欢月):ThreadLocal内存泄漏的原因

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/61241.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!