Threadlocal是否存在内存泄露

转自:Threadlocal是否存在内存泄露>

Threadlocal 是否存在内存泄露,需要解决以下的疑惑:

1. ThreadLocal.ThreadLocalMap 中提到的弱引用,弱引用究竟会不会被回收?

2. 弱引用什么情况下回收?

3. JAVA 的 ThreadLocal 和在什么情况下会内存泄露?

最近看到网上的一篇文章,分析说明 ThreadLoca l是如何内存泄露的。代码例子普遍是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal tl = new MyThreadLocal();
        tl.set(new My50MB());
          
        tl=null;
          
        System.out.println("Full GC");
        System.gc();
    }
      
    public static class MyThreadLocal extends ThreadLocal {
        private byte[] a = new byte[1024*1024*1];
          
        @Override
        public void finalize() {
            System.out.println("My threadlocal 1 MB finalized.");
        }
    }
      
    public static class My50MB {
        private byte[] a = new byte[1024*1024*50];
          
        @Override
        public void finalize() {
            System.out.println("My 50 MB finalized.");
        }
    
}

输出结果:

1
2
Full GC
My threadlocal 1 MB finalized.

这里已经模拟出了内存泄露的问题,可以看到 full gc 以后,内存还是被占用。我们想象 tl = null 将 threadlocal 的引用释放后,里面的 key、value 对象也释放,但是 value 却没有被释放。

很多人就开始分析了:threadlocal 里面使用了一个存在弱引用的 map,当释放掉 threadlocal 的强引用以后,map里面的 value 却没有被回收。而这块 value 永远不会被访问到了,所以存在着内存泄露。最好的做法是将调用 threadlocal 的 remove 方法。

说的也比较正确,当 value 不再使用的时候,调用 remove 的确是很好的做法,但内存泄露一说却不正确。这是 threadlocal 的设计的不得已而为之的问题。

首先,让我们看看在 threadlocal 的生命周期中,都存在哪些引用吧。看下图:实线代表强引用,虚线代表弱引用。

1
2
3
4
5
6
7
8
9
10
11
12
static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal> {
        Object value;
  
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
  
    private Entry[] table;
}

每个 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 的强引用链条才会断掉。

ThreadLocal.ThreadLocalMap 中提到的弱引用,弱引用究竟会不会被回收? 会被回收,key 为 null 的情况,在经历过 full gc 后,key 都被回收了。

弱引用什么情况下回收? 弱引用在 gc 时,被扫描到就会被回收,但是有一个前提,该弱引用在外部没有被引用到。

看下面改进后的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test2 {
  
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() { 
            @Override
            public void run() {
                ThreadLocal tl = new MyThreadLocal();
                tl.set(new My50MB());
                  
                tl=null;
                  
                System.out.println("Full GC");
                System.gc();
                  
            }             
        }).start();
                   
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000); 
    
}

输出结果:

1
2
3
Full GC
My threadlocal 1 MB finalized.
My 50 MB finalized.

我们可以看到,所有的都回收了。为什么要多次调用 system.gc()  这和 finalize 方法的策略有关系。finalize 是一个特别低优先级的线程,当执行 gc 时,如果一个对象需要被回收,先执行它的 finalize 方法。这意味着,本次 gc 可能无法真正回收这个具有 finalize 方法的对象,留待下次回收。这里多次调用 system.gc 正是为了给 finalize 留些时间。

JAVA 的 ThreadLocal 和在什么情况下会内存泄露?从上面的例子可以看出,当线程退出以后,我们的 value 被回收了,这是正确的。这说明内存并没有泄露。

转载请并标注: “本文转载自 linkedkeeper.com (文/张松然)”

此条目发表在java分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注