之前在看
java guide
上ThreadLocal
的相关内容时,看的云里雾里,当时只知道和ThreadLocal
底层使用的ThreadLocalMap
中Entry
的弱引用有关,甚至天真的以为ThreadLocal
内存泄漏的原因是弱引用(面试被问到也这么答了/(ㄒoㄒ)/~~😓),今天认真看了一下ThreadLocalMap
的源码,才发现并不是这样,而且网上很多错误回答也的确误导了当时的我,究其原因还是自己对于知识的掌握太过心切,欲速则不达,下面就来好好复盘一下这个问题
ThreadLocal用法
首先要知道Thread
类中有一个 threadLocals
和 一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,可以把 ThreadLocalMap
理解为ThreadLocal
类实现的定制化的 HashMap
:
1 | //Thread类中有两个ThreadLocalMap类型的变量 |
默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建它们,即当前线程调用这两个方法的时候,实际上调用的是静态内部类ThreadLocalMap
中对应的 get()
、set()
方法
这篇文章重点在于ThreadLocal
的内存泄漏问题,关于ThreadLocal
的定义与使用,就简单的回顾一下,先来看一个声明了多个ThreadLocal
变量的例子(借助ChatGPT):
1 | public class ThreadLocalExample { |
在这个例子中,我们声明了两个ThreadLocal
变量(属于类):threadLocal1
和threadLocal2
,每个ThreadLocal
变量都与当前线程的ThreadLocalMap
实例相关联
首先,我们来分析主线程的ThreadLocalMap
情况:
1 | Main Thread: |
主线程的ThreadLocalMap
中存储了两个键值对,分别是ThreadLocal1 -> "Hello"
和ThreadLocal2 -> 42
。这些值是在main
方法中通过threadLocal1.set("Hello")
和threadLocal2.set(42)
设置的
接下来,我们分析thread1
线程的ThreadLocalMap情况:
1 | Thread 1: |
thread1
线程的ThreadLocalMap
中存储了两个键值对,分别是ThreadLocal1 -> "Thread 1"
和ThreadLocal2 -> 10
。这些值是在thread1的线程体内通过threadLocal1.set("Thread 1")
和threadLocal2.set(10)
设置的
最后,我们分析thread2
线程的ThreadLocalMap
情况:
1 | Thread 2: |
thread2
线程的ThreadLocalMap
中也存储了两个键值对,分别是ThreadLocal1 -> "Thread 2"
和ThreadLocal2 -> 20
。这些值是在thread2的线程体内通过threadLocal1.set("Thread 2")
和threadLocal2.set(20)
设置的。
由上面可以看出,每个线程独立地管理其自己的ThreadLocalMap
,因此每个线程对ThreadLocal变量的更改不会影响其他线程的ThreadLocal变量,这样可以确保在多线程环境下,每个线程都具有独立的上下文信息
Java 引用
Java数据类型只有两种:基本类型和引用类型,例如:
1 | Object obj=new Object(); |
obj
就是引用类型,指向堆中的实例对象
而Java为了避免出现OutOfMemoryError
错误,会执行Garbage Collection
来不定时回收无任何对象引用的对象占据的内存空间,JVM如何找到需要回收的对象,方式有两种:
- 引用计数法:每个对象有一个引用计数属性,新增一个对该对象的引用时计数器加1,引用释放时计数减1,计数为0时则可以进行垃圾回收
- 可达性分析法:从
GC Roots
开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots
没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就判断是可回收对象
从JDK 1.2
版本开始,对象的引用被划分为4
种级别,从而使程序能更加灵活地控制对象的生命周期。这4
种级别由高到低依次为:强引用、软引用、弱引用和虚引用:
- 强引用 ** 是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器**绝不会回收它。如下:
1 | Object strongReference = new Object(); |
如果强引用对象不使用时,需要弱化从而使GC
能够回收,显式地设置strongReference
指向为null
,则JVM
认为该对象不存在引用,这时就可以回收这个对象:
1 | strongReference = null; |
如果在一个方法的内部有一个强引用,这个引用保存在Java
栈中,而真正的实例(Object
)保存在Java
堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0
,这个对象会被回收
1 | public void test() { |
但是如果这个strongReference
是全局变量时,就需要在不用这个对象时赋值为null
,因为强引用不会被垃圾回收!
- 软引用 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用,对比一下强引用和软引用,软引用需要借助
SoftReference
:
1 | // 强引用 |
软引用可以和一个引用队列(ReferenceQueue
)联合使用。如果软引用所引用对象被垃圾回收,JAVA
虚拟机就会把这个软引用加入到与之关联的引用队列:
1 | ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();//引用队列 |
即当内存不足时,JVM
首先将软引用中的对象引用置为null
,然后通知垃圾回收器进行回收,且垃圾收集线程会在JVM
抛出OutOfMemoryError
之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象(引入引用队列的原因)
- 弱引用 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
1 | String str = new String("abc"); |
弱引用同样会关联一个引用队列,如果弱引用所引用的对象被垃圾回收,Java
虚拟机就会把这个弱引用加入到与之关联的引用队列中,WeakReference
对象的生命周期基本由垃圾回收器决定,一旦垃圾回收线程发现了具有弱引用的对象,在下一次GC
过程中就会对其进行回收,本文所要讨论的ThreadLocalMap
中Entry
的Key
就是弱引用:
内存泄漏
不再会被使用的对象或者变量占用的内存不能被有效垃圾回收,就是内存泄露
1 | private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); |
观察以上代码:ThreadLocalMap
在保存时,key—threadlocal1
是一个弱引用,指向真正的堆中ThreadLocal实例,而value—"hello"
为当前线程变量的副本,是强引用。key被设计成WeakReference
弱引用,这就导致了一个问题:Entry
中的value
是强引用、而key
是弱引用,在JVM
发生GC
时key
会被回收,而value
由于是强引用不会,这样一来,ThreadLocalMap
中就会出现 key
为 null
的 Entry
,只有当前Thread线程退出以后,value的强引用链条才会断掉。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就产生了内存泄露
在TheadLocal
中内存泄漏是指TheadLocalMap
中的Entry
中的key
为null
,而value
不为null。因为key为null导致value一直访问不到,而根据可达性分析,始终有threadRef
->currentThread
->threadLocalMap
->entry
->valueRef
->valueMemory
,导致垃圾回收在进行可达性分析时,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏
而ThreadLocalMap
的实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法,ThreadLocal在remove()
的时候,会调用弱引用中的clear()
方法:
1 | //ThreaLocal中的remove方法 |
可以看出来:
Thread
类中包含变量ThreadLocalMap
,因此ThreadLocalMap
与Thread
的生命周期是一样长,如果没有手动删除对应key
,都会导致内存泄漏
但是使用弱引用可以多一层保障:ThreadLocal
实例对应的key
采用弱引用不会发生内存泄漏,对应的value
在下一次ThreadLocalMap
调用set(),get(),remove()
的时候会经过内部判断从而被清除
因此,ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread一样长,如果当前线程没有消亡,且没有手动删除对应key
为null
的entry
就会导致内存泄漏,而不是因为key是弱引用!