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/10/8 8:25:09 阅读更多 →
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/10/8 8:25:07 阅读更多 →
模拟赛车驾驶舱DIY:openrig开源铝型材方案从选材到调校

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

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

2026/10/8 8:25:04 阅读更多 →

最新新闻

基于GLM的Infra Agent实测:RSI三维评估,基础设施自动化运维的替代拐点将至

基于GLM的Infra Agent实测:RSI三维评估,基础设施自动化运维的替代拐点将至

凌晨2点47分,我盯着屏幕上一条告警:K8s集群某个节点 NotReady。放在以前,我的肌肉记忆是打开监控、翻日志、 ssh 上去手动排障,运气好半小时恢复,运气不好吵醒整个值班群。但这次我什么都没做——我把这个故障交给了基…

2026/10/8 9:43:42 阅读更多 →
numpy迭代数组nditer的实现示例

numpy迭代数组nditer的实现示例

前言 NumPy 是第三方库,用之前需要 pip install numpy;本机没有 Python 解释器也没有装 NumPy,所以下面的示例无法在本机运行验证,只能逐行人工推演,行为描述以 NumPy 官方文档为准。 先说清楚 nditer 是用来干什么的&…

2026/10/8 9:43:42 阅读更多 →
手机跑350亿参数大模型:内存墙、量化与实测全记录

手机跑350亿参数大模型:内存墙、量化与实测全记录

最近一直在折腾一件事:把 350 亿参数的大模型,真正跑在一台手机上。不是远程调 API,不是云侧推理,而是把模型文件下载到手机里,所有计算都发生在端侧。“内存墙”这个词,如果你自己动手跑过大模型&#xff…

2026/10/8 9:43:42 阅读更多 →
C#实现Hex2Bin:嵌入式工程师的烧录与OTA转换利器

C#实现Hex2Bin:嵌入式工程师的烧录与OTA转换利器

简介:这是一款面向嵌入式与单片机开发者的Hex转Bin转换小工具,完整包含C#源码、Visual Studio工程文件与可直接运行的程序。该工具以C#写成,程序逻辑清晰,便于阅读和二次修改。它针对Hex文件带地址信息、Bin文件更通用的特性&…

2026/10/8 9:43:42 阅读更多 →
个人AI助手代理实战指南:从目标解析到反馈闭环

个人AI助手代理实战指南:从目标解析到反馈闭环

1. 项目概述:当“个人AI助手”从概念变成真实战场“个人AI助手代理大战已经打响”——这句话不是媒体标题党,而是我过去三个月在真实场景里反复验证过的事实。它背后没有宏大叙事,只有一个个具体的人,在自己的工作流、学习链、生活…

2026/10/8 9:43:42 阅读更多 →
系统架构图怎么画?以供应链系统为例拆解模块边界与绘制步骤

系统架构图怎么画?以供应链系统为例拆解模块边界与绘制步骤

写架构图之前,先想清楚一个问题:你是为了“交付一张图”,还是为了“讲明白一个系统”。我见过太多人对着白板画了一下午,最后产出一张谁也看不懂的方框连线图,问题就出在没想清楚架构图到底解决什么问题。这篇文章用供…

2026/10/8 9:42:40 阅读更多 →

日新闻

抖音用户主页视频数据爬虫详解:点赞、收藏、分享字段抓取与 TaoToken 统一 Key 配置

抖音用户主页视频数据爬虫详解:点赞、收藏、分享字段抓取与 TaoToken 统一 Key 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:00:03 阅读更多 →
AI 编程 Trae 国内版与国际版一篇讲透:TaoToken 统一 Key 接入实测

AI 编程 Trae 国内版与国际版一篇讲透:TaoToken 统一 Key 接入实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:00:06 阅读更多 →
Claude Desktop 配置第三方推理接口教程:用 TaoToken 统一 Key 打通 API 调用

Claude Desktop 配置第三方推理接口教程:用 TaoToken 统一 Key 打通 API 调用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 0:00:07 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 14:34:12 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 14:34:13 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:29:10 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 14:34:12 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 11:43:46 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →