ThreadLocal内存泄漏分析

使用场景

  • 在我们想让线程间数据不共享,比如:数据库连接、Session 管理,会想到ThreadLocal可以实现我们需求。但是使用不当会出现内存泄露情况。

本地模拟情况

  • 代码如下
static class LocalVariable {
        byte[] s = new byte[1024*1024 * 1000];

    }

    static ThreadLocal<LocalVariable> local = new ThreadLocal<>();

    public static void main(String[] args) throws IOException {
        local.set(new LocalVariable());
//        local.remove();
        System.in.read();
    }

当我们执行一次GC出现如下界面
在这里插入图片描述

  • 当加上remove方法后的代码如下
static class LocalVariable {
        byte[] s = new byte[1024*1024 * 1000];

    }

    static ThreadLocal<LocalVariable> local = new ThreadLocal<>();

    public static void main(String[] args) throws IOException {
        local.set(new LocalVariable());
        local.remove();
        System.in.read();
    }

当我们也执行一次GC出现如下界面
在这里插入图片描述

  • 总结:当我们主动调用remove()方法后执行GC的时候会将我们的内存释放掉。

原因分析

  • 看下面图片
    在这里插入图片描述
    我们会发现key是一个弱引用,而值是一个强应用。为啥key是弱引用的情况下,GC回收不了呢?带着问题我们跟下源码

源码分析

调用set方法执行如何代码

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看出当当前线程作为key生成ThreadLocalMap对象,然后设置value。进入getMap后源码如下

/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

返回当前线程持有的ThreadLocalMap,ThreadLocalMap源码如下
在这里插入图片描述
我们看到ThreadLocalMap只是针对key做了弱引用,value仍然被存活的线程持有。在线程还存活的情况下GC是无法回收value的内存的。

  • 为啥我们调用remove后就可以释放内存呢?remove执行代码如下
/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

我们重点看下 m.remove(this)

/**
         * Remove the entry for key.
         */
        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;
                }
            }
        }

我们可以看到调用了e.clear();来清除Entry。所以在执行GC后会释放内存。

为啥key要设置成弱引用

在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的。会多一层保障

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值