最近看到网上的一篇文章,分析说明ThreadLocal是如何内存泄露的. 但我不这么认为. ThreadLocal设计的很好,根本不存在内存泄露问题. 本文就结合图和代码的例子来验证我的看法.
网上的代码例子普遍是这样子的:
01 |
public class Test { |
02 |
public static void main(String[] args) throws InterruptedException { |
03 |
ThreadLocal tl = new MyThreadLocal(); |
04 |
tl.set( new My50MB()); |
05 |
|
06 |
tl= null ; |
07 |
|
08 |
System.out.println( "Full GC" ); |
09 |
System.gc(); |
10 |
} |
11 |
|
12 |
public static class MyThreadLocal extends ThreadLocal { |
13 |
private byte [] a = new byte [ 1024 * 1024 * 1 ]; |
14 |
|
15 |
@Override |
16 |
public void finalize() { |
17 |
System.out.println( "My threadlocal 1 MB finalized." ); |
18 |
} |
19 |
} |
20 |
|
21 |
public static class My50MB { |
22 |
private byte [] a = new byte [ 1024 * 1024 * 50 ]; |
23 |
|
24 |
@Override |
25 |
public void finalize() { |
26 |
System.out.println( "My 50 MB finalized." ); |
27 |
} |
28 |
} |
29 |
30 |
} |
Full GC
My threadlocal 1 MB finalized.
很多人就开始分析了: threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法.
说的也比较正确,当value不再使用的时候,调用remove的确是很好的做法.但内存泄露一说却不正确. 这是threadlocal的设计的不得已而为之的问题.
首先,让我们看看在threadlocal的生命周期中,都存在哪些引用吧. 看下图: 实线代表强引用,虚线代表弱引用.
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 像上面code中的例子,当把threadlocal实例tl置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
从中可以看出,弱引用只存在于key上,所以key会被回收. 而value还存在着强引用.只有thead退出以后,value的强引用链条才会断掉. 看下面改进后的例子.
01 |
public class Test2 { |
02 |
03 |
/** |
04 |
* @param args |
05 |
* @throws InterruptedException |
06 |
*/ |
07 |
public static void main(String[] args) throws InterruptedException { |
08 |
new Thread( new Runnable() { |
09 |
10 |
@Override |
11 |
public void run() { |
12 |
ThreadLocal tl = new MyThreadLocal(); |
13 |
tl.set( new My50MB()); |
14 |
|
15 |
tl= null ; |
16 |
|
17 |
System.out.println( "Full GC" ); |
18 |
System.gc(); |
19 |
|
20 |
} |
21 |
|
22 |
}).start(); |
23 |
|
24 |
|
25 |
System.gc(); |
26 |
Thread.sleep( 1000 ); |
27 |
System.gc(); |
28 |
Thread.sleep( 1000 ); |
29 |
System.gc(); |
30 |
Thread.sleep( 1000 ); |
31 |
32 |
} |
33 |
34 |
} |
Full GC
My threadlocal 1 MB finalized.
My 50 MB finalized.
从上面的例子可以看出,当线程退出以后,我们的value被回收了. 这是正确的.这说明内存并没有泄露. 栈中还存在着对value的强引用路线.只是由于thread没有提供public接口,无法访问此value,但我们可以使用反射拿到这个value.
这也是不得已而为之的设计吧. 总之,如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧. 让我们好好思考一下,有什么办法可以在tl=null的时候,也释放value呢?