如果我们在使用ThreadLocal的过程中发现有内存泄漏的情况,是不是这个内存泄漏跟Entry中使用弱引用的key有关系?下面我们先来复习下内存泄漏和弱引用相关的知识,在分析。
Java中的引用类型有4种类型:强、软、弱、虚。当前问题主要涉及强引用和弱引用。
我们来分析下如果ThreadLocalMap的Entry的key是强引用的情况,情况会如何呢?此时ThreadLocal的内存堆栈图如下4.1-1所示:
因为ThreadLocal Entry中的key强引用了ThreadLocal导致ThreadLocal对象无法被回收
在没有手动删除这个Entry以及当前线程运行的情况下,始终有强引用链,CurrentThread Ref->CurrentThread->ThreadLocalMap->Entry(entry中包括key引用和value),导致Entry内存泄露。
ThreadLocalMap Entry中的key如果使用强引用,无法完全避免内存泄漏。
以上两种情况,内存泄漏的发生跟ThreadLocalMap key是否使用弱引用没有必然联系,那么真正原因是什么呢?
第一点如果在使用完ThreadLocal并调用其remove方法删除对应的Entry,可以避免内存泄漏。
第二点ThreadLocalMap 是Thread的一个属性,被当前线程引用,所以它的生命周期同Thread一样。如果在使用完ThreadLocal同时,线程结束运行,ThreadLocalMap也会被GC回收,根源上避免了内存泄露。
综上,THreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期同Thread一样长,在使用完ThreadLocal后如果没有手动删除对应的Entry就会导致内存泄漏。
根据上面的分析,Entry的key无论是使用强弱引用,都可能导致ThreadLocal的内存泄漏。避免内存泄露的方法:
显然相对于第一种方式,第二种方式更不好控制,特别是在使用线程池的情况下,线程结束执行是不会被销毁的。
而我们只要在使用完ThreadLocal后,调用remove方法,无论Entry的key是强引用还是弱引用,都不会出现ThreadLocal内存泄露的情况,为什么我们还要使用弱引用呢?
根据我们之前对ThreadLocal源码的分析,ThreadLocalMap的set、getEntry方法中,都会对key为null的entry进行标记回收(非实时),即吧key为null对应entry的vualue置为null。这意味着,在当前线程依然运行的前提下,就算没有忘记调用remove方法,弱引用比强引用可以多一层保障:在ThreadLocal引用被回收之后,对应Entry中的key的弱引用ThreadLocal被回收,对应的value在下一次ThreadLocalMap调用set、get或者remove任一方法的时候会被清除,从而避免内存泄漏。
虽然key为弱引用为避免内存泄漏提供一层保障,但不是实时的,需要在下次调用ThreadLocalMap的相应的方法的时候才会被清除。所以在ThreadLocal使用完成后,强烈建议调用remove方法。
首先我们来看下ThreadLocalMap是如果计算hash的,构造方法如下:
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
// 计算hash
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
这里通过定义一个AtomicInteger类型,每次获取当前值加上HASH_INCREMENT,这个值跟斐波那契数列(黄金分割数)有关,主要目的使哈希码均匀的分布在 2 n 2^n 2n的数组中,尽量避免hash冲突。
计算hash的时候采用hashcode&(size-1)的算法,这相当于hashcode%size 的一个更高效的实现。因此采用该算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash冲突的次数减少。
关于hash、散列以及黄金分割数更深入的知识,本人目前没学习,不做讨论啊。
回顾下之前的ThreadLocalMap的set方法,源代码如下;
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 线性探测法查找元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
方法通过线性探测法查找目标key,如果计算hash位置有元素,但是key与目标key不相等,即产生了hash冲突,那么继续后向查找key为空的元素,替换该元素。如果在遍历到元素为空,但是即没有找到key相等的元素,也没有元素key为空的情况,直接在元素为空的位置插入新的Entry(key,value)。
后向获取索引nextIndex()方法,源代码如下:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
即遍历到数组末尾的时候,会把索引置为0,从头开始,循环执行。
看下源代码:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 线性探测法查找元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
同set()一样,这里hash冲突之后,继续后向遍历,直到找到key和目标key相等的entry,回收清理或者元素为空。