1、ThreadLocal 真的会内存泄漏吗?
在网上去学习ThreadLocal经常看到,在不使用的时候需要调用remove()方法,否则会有内存泄漏。通过查找资料和阅读源码并验证(验证)发现是不会出现内存泄漏。
ThreadLocal存储将存储对象放置到Thread线程中,threadLocals 变量中,ThreadLocalMap是ThreadLocal的静态内部类
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal存放数据,是将 threadLoacl 本身作为Key存放存在ThreadLocalMap中,value是ThreadLocal放置的值。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
获取ThreadLocal存在对象
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();
}
内存泄漏论,有两个内存泄漏的点
①、ThreadLocal 如果一直存在,不被回收,与它关联的ThreadLocalMap中的存储对象时Entry会一直互相引用不被回收。
②、Thread线程如果一直存在,ThreadLocal不会被回收。
③、如果线程一直存在,Thread 中的ThreadLocalMap变量的Entry不能被GC回收掉的,虽然是entry的key是软引用会被回收掉,但是value不会被回收,如果Thread一直存在就会内存泄漏。
这里不得不叹服大师们武功的出神入化高深莫测,Josh Bloch 和 Doug Lea 使用弱引用巧妙的解决了前两个问题。
截取一部分 ThreadLocalMap的代码,可以看出使用了弱引用 WeakReference
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
2、什么是弱引用?
弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
强引用、软引用、弱引用、幻象引用有什么区别?有哪些使用场景?
3、ThreadLoad使用弱引用,会不会被GC误回收
当然是不会的。上文提到ThreadLocalMap中的Entry 的key 是threadLocal对象本身,但是会被作为弱引用
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
上面是实现代码
4、为什么不会被回收掉呢?
因为虽然Entry的Key是弱引用,但是Key指向的是强引用,然后出现每次GC之前都会去尝试回收掉KEY,然后回收不掉。使用软引用将ThreadLocal对象和ThrealLocal.Entry对象的强引用关系 ,转变为弱引用关系。大家都知道强引用关系,GC标记是引用可达,他们之间有任何一个还在使用就不会被回收,换成弱引用之后,两个对象之间就不是强引用关系,可以各自保证被回收掉。
5、验证ThreadLocal会被GC回收
环境JDK8,idea,JDK自带工具jvisualvm.exe
①:Thread线程如果一直存在,ThreadLocal不会被回收
请看代码注释理解,
public class Test {
public static void main(String[] args) {
new Thread(() -> {
//ThreadLocal放置到 代码块中,为证明 threadLocal 会被GC回收掉
{
ThreadLocalTest threadLocal = new ThreadLocalTest<>();
threadLocal.set(new AlarmEmailVO());
System.out.println(threadLocal.get());
}
int i = 0;
while (true) {
try {
Thread.sleep(1000);
//每秒加10M 堆内存 能看出内存增长趋势
//GC时,a临时变量会被回收
int[] a = new int[1024 * 1024 * 10];
if (i++ > 100) {
break;
}
i++;
} catch (InterruptedException e) {
}
}
}).start();
}
}
class AlarmEmailVO {
@Override
public String toString() {
return "AlarmEmailVO--AlarmEmailVO";
}
}
/**
* 继承ThreadLocal 加入100M的变量,方便当发生GC时,通过堆内存曲线发现是否被回收掉
*
* @param
*/
class ThreadLocalTest extends ThreadLocal {
int[] bb = new int[1024 * 1024 * 100];
}
通过使用jvisualvm手动触发GC发现,ThreadLocall对象被回收了,因为不被回收的话,堆内存至少100M
②、ThreadLocal 如果一直存在,不被回收,与它关联的ThreadLocalMap中的存储对象时Entry会一直互相引用不被回收。
public class Test {
public static void main(String[] args) throws InterruptedException {
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set(new AlarmEmailVO());
System.out.println(threadLocal.get());
new Thread(() -> {
int i = 0;
while (true) {
try {
//支持一段时间,便于观察对内存变化
Thread.sleep(1000);
if (i++ > 30) {
break;
}
i++;
} catch (InterruptedException e) {
}
}
}).start();
Thread.sleep(10000);
}
}
class AlarmEmailVO {
//AlarmEmailVO 中的临时变量,为了证明Thread中的ThreadLocalMap 会被回收,对内存会有明细变化
int[] bb = new int[1024 * 1024 * 100];
@Override
public String toString() {
return "AlarmEmailVO--AlarmEmailVO";
}
}
记得持续手动执行GC
③、如果线程一直存在,Thread 中的ThreadLocalMap变量的Entry不能被GC回收掉的
package com.sp;
public class Test {
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
{
ThreadLocalTest threadLocal = new ThreadLocalTest<>();
threadLocal.set(new AlarmEmailVO());
System.out.println(threadLocal.get());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
int i = 0;
while (true) {
try {
//支持一段时间,便于观察对内存变化
Thread.sleep(1000);
byte[] bb = new byte[1024 * 1024 * 20];
if (i++ > 30) {
break;
}
} catch (InterruptedException e) {
}
}
}).start();
Thread.sleep(10000);
}
}
class AlarmEmailVO {
//AlarmEmailVO 中的临时变量,为了证明Thread中的ThreadLocalMap 会被回收,对内存会有明细变化
byte[] bb = new byte[1024 * 1024 * 100];
@Override
public String toString() {
return "AlarmEmailVO--AlarmEmailVO";
}
}
class ThreadLocalTest extends ThreadLocal{
byte[] bb = new byte[1024 * 1024 * 100];
}
Thread结束前,一直有100M内存,所以ThreadLocal的对象没有被回收掉
总结:
ThreadLocal会出现内存泄漏,但是已经提供了 remove()方法ThreadLocal不用之后调用remove()方法,删除掉该对象。
推荐文章:
正确理解Thread Local的原理与适用场景 这是转发的文章
用弱引用堵住内存泄漏