用代码实战,彻底搞清楚ThreadLocal发生内存泄漏的情况。很多文章讲的模棱两可,在和群友的沟通中,基本弄清楚了ThreadLocal到底是什么回事,解决大多数文章都无法把知识点和实际使用结合起来讲。
/** * 测试threadLocal内存泄漏 * 01:固定6个线程,每个线程持有一个变量 * 按理来说会有 6 * 5 = 30M内存无法回收,其余的在set方法中覆盖了。 */ public class ThreadLocalOutOfMemoryTest { static class LocalVariable { //总共有5M private byte[] locla = new byte[1024 * 1024 * 5]; } // (1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行 final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); // (2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组 static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); public static void main(String[] args) throws InterruptedException { // (3)向线程池里面放入 50 个任务 for (int i = 0; i < 50; ++i) { poolExecutor.execute(new Runnable() { @Override public void run() { // (4) 往threadLocal变量设置值 LocalVariable localVariable = new LocalVariable(); // 会覆盖 ThreadLocalOutOfMemoryTest.localVariable.set(localVariable); // (5) 手动清理ThreadLocal System.out.println("thread name end:" + Thread.currentThread().getName() + ", value:"+ ThreadLocalOutOfMemoryTest.localVariable.get()); // ThreadLocalOutOfMemoryTest.localVariable.remove(); } }); Thread.sleep(1000); } // (6)是否让key失效,都不影响。只要持有的线程存在,都无法回收。 //ThreadLocalOutOfMemoryTest.localVariable = null; System.out.println("pool execute over"); } } 复制代码
jvisualvm.exe
,在运行这个小例子。等待程序结束,主动触发一次垃圾回收。我们可以看到,大概还有30M的内存没有回收。和我们预期的结论一致。
a.为什么不是50*5=250M 呢?
因为java.lang.ThreadLocal#set()
是会覆盖的,每个线程持有一份,同一个线程执行多次,由于key是同一个ThreadLocal变量,所以会路由到数组的同一个位置上,直接覆盖上次的value。
b.为什么无法被回收掉30M的内存呢?
因为线程池中的6个线程存活,对ThreadLocalMap持有强引用
这个很好理解,因为我们手动调用了remove方法,清理了ThreadLocalMap中的对象(把Entry对象指向了null),没有了强引用,当然直接被回收。
和第一种情况一致,任然还有30M的内存无法回收。所以ThreadLocalMap的回收其实和Entry对象的Key是弱引用没有太大关系。
只要持有ThreadLocalMap的线程存活,不管Key失效或者未失效,value都不会被回收。所以才要求我们使用的过程中要即时清理。
ThreadLocal local = new ThreadLocal(); local = null; 复制代码
问题:③这里为什么要使用弱引用呢?
假设一种在Tomcat线程池使用ThradLocal的场景,线程持有对ThreadLocalMap的强引用,导致ThreadLocalMap的生命周期很长。假设为每一个请求创建一个ThreadLocal对象用于存储session,如果不及时销毁,整个ThreadLocalMap占用的内存会越来越大。 提前把Key设置为thradLocal对象的弱应用,当程序不在引用threadLocal对象的时候,gc就可以快速回收掉thradLocal对象,Entry的key为null。在ThreadLocalMap#set方法中会清理key=null的Entry对象,以达到回收内存的目的。
threadLoca=null
threadLoca=null
,所以是否回收ThreadLocalMap主要取决于线程的生命周期。
作者:城南码农
链接:https://juejin.cn/post/6982121384533032991
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。