ThreadLocal系列(二):内存泄漏与remove
ThreadLocal内存泄漏上个文章说到为什么最开始有用到remove方法是为了防范ThreadLocal内存泄漏。从这段代码可以看到map的key和value的引用关系不同GC回收的时候并不会回收value导致key是null但value指的对象却没释放Thread可能长时间运行比如线程池里的value堆积之后会发生内存泄漏。static class Entry extends WeakReferenceThreadLocal? { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal? k, Object v) { super(k); value v; } }其中key弱引用ThreadLocal对象而value强引用实际的对象。为什么要这么设计GC垃圾回收的时候会回收弱引用所指向的对象即referentThreadLocal对象本身被回收而弱引用对象Entry本身不会被回收。而在set和get的时候发现要放入的entry对应的Key是null就把value进行回收也就是说GC回收了Entry弱引用所指向的ThreadLocal对象导致e.get()返回nullvalue还在而在下一次setget的时候才会回收强引用的valueset清理private void set(ThreadLocal? key, Object value) { // We dont 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) { //发现key为null准备回收value replaceStaleEntry(key, value, i); return; } } tab[i] new Entry(key, value); int sz size; if (!cleanSomeSlots(i, sz) sz threshold) rehash(); }而在replaceStaleEntry里有这么一段private void replaceStaleEntry(ThreadLocal? key, Object value, int staleSlot) { Entry[] tab table; int len tab.length; Entry e; //省略。。。 // If key not found, put new entry in stale slot tab[staleSlot].value null; tab[staleSlot] new Entry(key, value); //省略。。。 }get清理public T get() { Thread t Thread.currentThread(); ThreadLocalMap map getMap(t); if (map ! null) { //关键getEntry ThreadLocalMap.Entry e map.getEntry(this); if (e ! null) { SuppressWarnings(unchecked) T result (T)e.value; return result; } } return setInitialValue(); }进入getEntryprivate Entry getEntry(ThreadLocal? key) { int i key.threadLocalHashCode (table.length - 1); Entry e table[i]; if (e ! null e.get() key) return e; else return getEntryAfterMiss(key, i, e); // 没命中进入miss }再进入getEntryAfterMissprivate Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) { Entry[] tab table; int len tab.length; while (e ! null) { ThreadLocal? k e.get(); if (k key) return e; if (k null) //找到了当key为null的时候会调用这个方法进入 expungeStaleEntry(i); else i nextIndex(i, len); e tab[i]; } return null; }进入expungeStaleEntryprivate int expungeStaleEntry(int staleSlot) { Entry[] tab table; int len tab.length; // expunge entry at staleSlot // 这个staleSlot就是上面传的i会把这个key为null的value清除掉 tab[staleSlot].value null; // value置为null tab[staleSlot] null; // Entry置为null size--; //继续往后找并清理脏entry // Rehash until we encounter null Entry e; int i; for (i nextIndex(staleSlot, len); (e tab[i]) ! null; //往后继续清理直到遇到entry为null才停 i nextIndex(i, len)) { ThreadLocal? k e.get(); if (k null) { // Key又是null脏entry,准备回收value e.value null; tab[i] null; size--; } else { int h k.threadLocalHashCode (len - 1); // 重新计算理想槽位 if (h ! i) { // 理想槽位 ! 当前位置说明之前是hash冲突被挤过来的上个文章说过它用的是开放定址法 tab[i] null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] ! null) h nextIndex(h, len); tab[h] e; } } } return i; }从上面可以看到get的时候不仅会清理自己的还会清理后面的。set类似不同于getset还会扫描前面的entry所以ThreadLocal的setget也对内存泄漏做了防范措施但是这其实是为了防范编程习惯导致的漏洞实际上在使用ThreadLocal的时候就应该养成手动调用remove方法的习惯ThreadLocal里的remove方法public void remove() { ThreadLocalMap m getMap(Thread.currentThread()); if (m ! null) m.remove(this); }进入removeprivate void remove(ThreadLocal? key) { Entry[] tab table; int len tab.length; int i key.threadLocalHashCode (len-1); // 算hash槽位 for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { if (e.get() key) { e.clear(); // WeakReference.clear()把key置null expungeStaleEntry(i); //清理当前往后到null return; } } }OOMpublic class TestOOM { public static void main(String[] args) throws InterruptedException { int round 0; while ((true)) { round; //在ThreadLocal里创建大对象50MB ThreadLocalbyte[] threadLocal new ThreadLocal(); threadLocal.set(new byte[1024 * 1024 * 50]); //调用GC回收弱引用所指向的对象 System.gc(); //等GC完成GC是单独的另一个线程先让main睡一会儿确保GC完成 Thread.sleep(100); //打印堆内存 Runtime r Runtime.getRuntime(); long usedMB (r.totalMemory() - r.freeMemory()) / 1024/1024; // MB System.out.println(第 round 轮已用堆: usedMB MB); } } }先设置JVM堆内存大小-Xmx300m -Xms300m -XX:PrintGCDetails运行结果[GC (System.gc()) [PSYoungGen: 57344K-856K(89600K)] 57344K-52064K(294400K), 0.0259066 secs] [Times: user0.01 sys0.00, real0.03 secs] [Full GC (System.gc()) [PSYoungGen: 856K-0K(89600K)] [ParOldGen: 51208K-51883K(204800K)] 52064K-51883K(294400K), [Metaspace: 3237K-3237K(1056768K)], 0.0154506 secs] [Times: user0.11 sys0.08, real0.02 secs] 第1轮已用堆: 61MB [GC (System.gc()) [PSYoungGen: 61995K-992K(89600K)] 113879K-104083K(294400K), 0.0145459 secs] [Times: user0.06 sys0.03, real0.01 secs] [Full GC (System.gc()) [PSYoungGen: 992K-0K(89600K)] [ParOldGen: 103091K-103895K(204800K)] 104083K-103895K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0146028 secs] [Times: user0.09 sys0.03, real0.01 secs] 第2轮已用堆: 101MB [GC (System.gc()) [PSYoungGen: 52156K-64K(89600K)] 156051K-155159K(294400K), 0.0152130 secs] [Times: user0.02 sys0.05, real0.02 secs] [Full GC (System.gc()) [PSYoungGen: 64K-0K(89600K)] [ParOldGen: 155095K-155093K(204800K)] 155159K-155093K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0294127 secs] [Times: user0.02 sys0.16, real0.03 secs] 第3轮已用堆: 151MB [Full GC (System.gc()) [PSYoungGen: 52349K-0K(89600K)] [ParOldGen: 155093K-103893K(204800K)] 207443K-103893K(294400K), [Metaspace: 6108K-6108K(1056768K)], 0.0083966 secs] [Times: user0.00 sys0.00, real0.01 secs] 第4轮已用堆: 108MB [GC (System.gc()) [PSYoungGen: 59287K-448K(89600K)] 163181K-155549K(294400K), 0.0155315 secs] [Times: user0.00 sys0.01, real0.02 secs] [Full GC (System.gc()) [PSYoungGen: 448K-0K(89600K)] [ParOldGen: 155101K-155389K(204800K)] 155549K-155389K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0311568 secs] [Times: user0.08 sys0.03, real0.03 secs] 第5轮已用堆: 151MB [Full GC (System.gc()) [PSYoungGen: 52491K-51200K(89600K)] [ParOldGen: 155389K-155389K(204800K)] 207880K-206589K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0155440 secs] [Times: user0.16 sys0.00, real0.02 secs] 第6轮已用堆: 201MB [Full GC (Ergonomics) [PSYoungGen: 52563K-51200K(89600K)] [ParOldGen: 155389K-155389K(204800K)] 207953K-206589K(294400K), [Metaspace: 8034K-8034K(1056768K)], 0.0046614 secs] [Times: user0.00 sys0.00, real0.01 secs] [Full GC (Allocation Failure) [PSYoungGen: 51200K-51200K(89600K)] [ParOldGen: 155389K-155235K(204800K)] 206589K-206435K(294400K), [Metaspace: 8034K-8013K(1056768K)], 0.0282928 secs] [Times: user0.06 sys0.08, real0.03 secs] Heap PSYoungGen total 89600K, used 54775K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000) eden space 76800K, 71% used [0x00000000f9c00000,0x00000000fd17dd80,0x00000000fe700000) from space 12800K, 0% used [0x00000000ff380000,0x00000000ff380000,0x0000000100000000) to space 12800K, 0% used [0x00000000fe700000,0x00000000fe700000,0x00000000ff380000) ParOldGen total 204800K, used 155235K [0x00000000ed400000, 0x00000000f9c00000, 0x00000000f9c00000) object space 204800K, 75% used [0x00000000ed400000,0x00000000f6b98c70,0x00000000f9c00000) Metaspace used 8060K, capacity 8286K, committed 8448K, reserved 1056768K class space used 947K, capacity 1015K, committed 1024K, reserved 1048576K Exception in thread main java.lang.OutOfMemoryError: Java heap space at threadlocal.TestOOM.main(TestOOM.java:17)看到上面第三轮到第四轮已用堆降下去了正如前面说的set的时候也会清理value在System.gc()前调用remove可以防止OOM这里就贴个结果。[GC (System.gc()) [PSYoungGen: 52692K-32K(89600K)] 54691K-2030K(294400K), 0.0003997 secs] [Times: user0.00 sys0.00, real0.00 secs] [Full GC (System.gc()) [PSYoungGen: 32K-0K(89600K)] [ParOldGen: 1998K-1998K(204800K)] 2030K-1998K(294400K), [Metaspace: 8635K-8635K(1056768K)], 0.0044947 secs] [Times: user0.00 sys0.00, real0.00 secs] 第52轮已用堆: 1MB [GC (System.gc()) [PSYoungGen: 52692K-32K(89600K)] 54691K-2030K(294400K), 0.0016404 secs] [Times: user0.00 sys0.00, real0.00 secs] [Full GC (System.gc()) [PSYoungGen: 32K-0K(89600K)] [ParOldGen: 1998K-1998K(204800K)] 2030K-1998K(294400K), [Metaspace: 8636K-8636K(1056768K)], 0.0064242 secs] [Times: user0.00 sys0.00, real0.01 secs] 第53轮已用堆: 1MB 省略。。。除了set,get以及手动调用remove防止内存泄漏还可以从弱引用方向思考如何防止OOM。当GC回收弱引用所指向的对象后对应的弱引用对象会被放入引用队列所以可以通过引用队列追踪并释放value。和ThreadLocal没太大关系只是从另一角度思考防止OOM

相关新闻

用 Pitchfork 做一次撞库:两次实验教会我的事

用 Pitchfork 做一次撞库:两次实验教会我的事

靶场:本地虚拟机 Metasploitable2 Kali,仅主机网络,全程在自有环境内操作。 一、先纠正一个误解:撞库不是"随机组合密码" 我刚开始学的时候,以为撞库就是把账号和密码随机排列组合去试。做过一遍之后才发现…

2026/11/1 18:34:55 阅读更多 →
Unsloth 微调 构建自己的大模型 没有GPU也能微调

Unsloth 微调 构建自己的大模型 没有GPU也能微调

这次我们使用unsloth框架来微调模型,将模型微调成一个垂直领域的模型先说问题This environment is externally managed ╰─> To install Python packages system-wide, try apt installpython3-xyz, where xyz is the package you are trying toinstall.从 Pyth…

2026/11/5 20:28:28 阅读更多 →
模拟赛车驾驶舱DIY:openrig开源铝型材方案从选材到调校

模拟赛车驾驶舱DIY:openrig开源铝型材方案从选材到调校

模拟赛车圈里,rig 是一个绕不开的词。它指的不是电脑主机,而是那套把方向盘、踏板、座椅和屏幕全部整合成一个固定驾驶环境的支架系统。openrig 就是奔着这件事来的一套方案:全开放的图纸,所有结构件用标准铝型材搭建,…

2026/11/9 2:20:44 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/15 5:13:35 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/15 5:13:35 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/15 5:13:35 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/15 3:37:41 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/15 4:18:38 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/15 5:13:35 阅读更多 →

日新闻

周新闻

月新闻