惊!ThreadLocal你怎么动不动就内存泄漏?

存储 存储软件
使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢?

[[382111]] 

本文转载自微信公众号「无聊学Java」,作者无聊  。转载本文请联系无聊学Java公众号。 

今天无聊带大家分析下ThreadLocal为什么会内存泄漏~

前言

使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢?

正文

我们首先看一个例子,代码如下:

  1. public class ThreadLocalOutOfMemoryTest { 
  2.     static class LocalVariable { 
  3.         private Long[] a = new Long[1024*1024]; 
  4.     } 
  5.  
  6.     // (1) 
  7.     final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES, 
  8.             new LinkedBlockingQueue<>()); 
  9.     // (2) 
  10.     final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); 
  11.  
  12.     public static void main(String[] args) throws InterruptedException { 
  13.         // (3) 
  14.         for (int i = 0; i < 50; ++i) { 
  15.             poolExecutor.execute(new Runnable() { 
  16.                 public void run() { 
  17.                     // (4) 
  18.                     localVariable.set(new LocalVariable()); 
  19.                     // (5) 
  20.                     System.out.println("use local varaible"); 
  21. //                    localVariable.remove(); 
  22.  
  23.                 } 
  24.             }); 
  25.  
  26.             Thread.sleep(1000); 
  27.         } 
  28.         // (6) 
  29.         System.out.println("pool execute over"); 
  30.     } 

代码(1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行。

代码(2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组。

代码(3)向线程池里面放入 50 个任务。

代码(4)设置当前线程的 localVariable 变量,也就是把 new 的 LocalVariable 变量放入当前线程的 threadLocals 变量。

由于没有调用线程池的 shutdown 或者 shutdownNow 方法所以线程池里面的用户线程不会退出,进而 JVM 进程也不会退出。

运行后,我们立即打开jconsole 监控堆内存变化,如下图:

接着,让我们打开 localVariable.remove() 注释,然后在运行,观察堆内存变化如下:

从第一次运行结果可知,当主线程处于休眠时候进程占用了大概 75M 内存,打开 localVariable.remove() 注释后第二次运行则占用了大概 25M 内存,可知 没有写 localVariable.remove() 时候内存发生了泄露,下面分析下泄露的原因,如下:

“第一次运行的代码,在设置线程的 localVariable 变量后没有调用localVariable.remove() 方法,导致线程池里面的 5 个线程的 threadLocals 变量里面的new LocalVariable()实例没有被释放,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出。这里需要注意的是由于 localVariable 被声明了 static,虽然线程的 ThreadLocalMap 里面是对 localVariable 的弱引用,localVariable 也不会被回收。运行结果二的代码由于线程在设置 localVariable 变量后即使调用了localVariable.remove()方法进行了清理,所以不会存在内存泄露。

接下来我们要想清楚的知道内存泄漏的根本原因,那么我们就要进入源码去看了。

我们知道ThreadLocal 只是一个工具类,具体存放变量的是在线程的 threadLocals 变量里面,threadLocals 是一个 ThreadLocalMap 类型的,我们首先一览ThreadLocalMap的类图结构,类图结构如下图:

如上图 ThreadLocalMap 内部是一个 Entry 数组, Entry 继承自 WeakReference,Entry 内部的 value 用来存放通过 ThreadLocal 的 set 方法传递的值,那么 ThreadLocal 对象本身存放到哪里了吗?

下面看看 Entry 的构造函数,如下所示:

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

接着我们再接着看Entry的父类WeakReference的构造函数super(k),如下所示:

  1. public WeakReference(T referent) { 
  2.    super(referent); 

接着我们再看WeakReference的父类Reference的构造函数super(referent),如下所示:

  1. Reference(T referent) { 
  2.    this(referent, null); 

接着我们再看WeakReference的父类Reference的另外一个构造函数this(referent , null),如下所示:

  1. Reference(T referent, ReferenceQueue<? super T> queue) { 
  2.    this.referent = referent; 
  3.    this.queue = (queue == null) ? ReferenceQueue.NULL : queue; 

可知 k 被传递到了 WeakReference 的构造函数里面,也就是说 ThreadLocalMap 里面的 key 为 ThreadLocal 对象的弱引用,具体是 referent 变量引用了 ThreadLocal 对象,value 为具体调用 ThreadLocal 的 set 方法传递的值。

当一个线程调用 ThreadLocal 的 set 方法设置变量时候,当前线程的 ThreadLocalMap 里面就会存放一个记录,这个记录的 key 为 ThreadLocal 的引用,value 则为设置的值。

但是考虑如果这个 ThreadLocal 变量没有了其他强依赖,而当前线程还存在的情况下,由于线程的 ThreadLocalMap 里面的 key 是弱依赖,则当前线程的 ThreadLocalMap 里面的 ThreadLocal 变量的弱引用会被在 gc 的时候回收,但是对应 value 还是会造成内存泄露,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项。

其实在 ThreadLocal 的 set 和 get 和 remove 方法里面有一些时机是会对这些 key 为 null 的 entry 进行清理的,但是这些清理不是必须发生的,下面简单讲解ThreadLocalMap 的 remove 方法的清理过程,remove 的源码,如下所示:

  1. private void remove(ThreadLocal<?> key) { 
  2.  
  3.   //(1)计算当前ThreadLocal变量所在table数组位置,尝试使用快速定位方法 
  4.   Entry[] tab = table
  5.   int len = tab.length; 
  6.   int i = key.threadLocalHashCode & (len-1); 
  7.   //(2)这里使用循环是防止快速定位失效后,变量table数组 
  8.   for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { 
  9.       //(3)找到 
  10.       if (e.get() == key) { 
  11.           //(4)找到则调用WeakReference的clear方法清除对ThreadLocal的弱引用 
  12.           e.clear(); 
  13.           //(5)清理keynull的元素 
  14.           expungeStaleEntry(i); 
  15.           return
  16.       } 
  17.    } 
  18.  private int expungeStaleEntry(int staleSlot) { 
  19.             Entry[] tab = table
  20.             int len = tab.length; 
  21.             //(6)去掉去value的引用 
  22.             tab[staleSlot].value = null
  23.             tab[staleSlot] = null
  24.             size--; 
  25.             Entry e; 
  26.             int i; 
  27.             for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { 
  28.                 ThreadLocal<?> k = e.get(); 
  29.                 //(7)如果keynull,则去掉对value的引用。 
  30.                 if (k == null) { 
  31.                     e.value = null
  32.                     tab[i] = null
  33.                     size--; 
  34.                 } else { 
  35.                     int h = k.threadLocalHashCode & (len - 1); 
  36.                     if (h != i) { 
  37.                         tab[i] = null
  38.                         while (tab[h] != null
  39.                             h = nextIndex(h, len); 
  40.                         tab[h] = e; 
  41.                     } 
  42.                 } 
  43.             } 
  44.             return i; 
  45.   } 

代码(4)调用了 Entry 的 clear 方法,实际调用的是父类 WeakReference 的 clear 方法,作用是去掉对 ThreadLocal 的弱引用。

代码(6)是去掉对 value 的引用,到这里当前线程里面的当前 ThreadLocal 对象的信息被清理完毕了。

代码(7)从当前元素的下标开始看 table 数组里面的其他元素是否有 key 为 null 的,有则清理。循环退出的条件是遇到 table 里面有 null 的元素。所以这里知道 null 元素后面的 Entry 里面 key 为 null 的元素不会被清理。

总结

  1. ThreadLocalMap 内部 Entry 中 key 使用的是对 ThreadLocal 对象的弱引用,这为避免内存泄露是一个进步,因为如果是强引用,那么即使其他地方没有对 ThreadLocal 对象的引用,ThreadLocalMap 中的 ThreadLocal 对象还是不会被回收,而如果是弱引用则这时候 ThreadLocal 引用是会被回收掉的。
  2. 但是对于的 value 还是不能被回收,这时候 ThreadLocalMap 里面就会存在 key 为 null 但是 value 不为 null 的 entry 项,虽然 ThreadLocalMap 提供了 set,get,remove 方法在一些时机下会对这些 Entry 项进行清理,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露,所以在使用完毕后即使调用 remove 方法才是解决内存泄露的最好办法。
  3. 线程池里面设置了 ThreadLocal 变量一定要记得及时清理,因为线程池里面的核心线程是一直存在的,如果不清理,那么线程池的核心线程的 threadLocals 变量一直会持有 ThreadLocal 变量。

 

 

责任编辑:武晓燕 来源: 无聊学Java
相关推荐

2020-09-10 07:40:28

ThreadLocal内存

2020-09-11 07:38:50

内存泄漏检测

2020-11-09 06:00:04

Windows 10Windows操作系统

2021-08-10 09:58:59

ThreadLocal内存泄漏

2018-10-25 15:24:10

ThreadLocal内存泄漏Java

2022-05-09 14:09:23

多线程线程安全

2024-03-22 13:31:00

线程策略线程池

2024-02-02 09:00:14

内存泄漏对象

2022-11-04 08:47:52

底层算法数据

2018-05-08 15:42:30

PC升级笔记本

2024-03-11 08:22:40

Java内存泄漏

2023-12-18 10:45:23

内存泄漏计算机服务器

2012-02-22 21:28:58

内存泄漏

2015-03-30 11:18:50

内存管理Android

2022-08-26 07:33:49

内存JVMEntry

2021-08-05 15:28:22

JS内存泄漏

2021-08-09 09:54:37

内存泄漏JS 阿里云

2019-01-30 18:24:14

Java内存泄漏编程语言

2020-06-08 09:18:59

JavaScript开发技术

2023-05-29 07:17:48

内存溢出场景
点赞
收藏

51CTO技术栈公众号