网站首页 > 技术文章 正文
说到内存溢出,我相信各位都知道是什么,但是说到内存泄露,而且还是 ThreadLocal ,阿粉就得来说一下这个了,毕竟如果面试的时候被问到 ThreadLocal 的内存泄露,是不是有可能不太了解了呢,今天阿粉来说一下这个 ThreadLocal 的内存泄露的原因,以及如何从开发中去避免这个问题。
什么是内存泄露
说到内存泄露,阿粉就得说一下,这个可能对于初中级的程序员来说,还是比较陌生的,为什么这么说,是因为,JVM 有自己的内存回收机制,所以对于初中级的程序员来说,很少有接触到这个的,而内存泄露的意思呢,就是为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
我们也都知道,我们有时候在定义变量的时候,就应该明白,他需要一段内存空间来存储这个数据信息,而这段内存如果一直不被释放,那么就会导致,内存被占用光,而被占用的这个对象,一直不能被回收掉,这就是内存泄漏。
在说 ThreadLocal 的内存泄露之前,我们先说说 ThreadLocal 的实现原理,然后我们再分析,他是泄露的原因是什么?
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
每一个 ThreadLocal 维护一个 ThreadLocalMap,key为使用 弱引用 的 ThreadLocal 实例,value为线程变量的副本。
那么什么又是弱引用呢?
引用的话,其实分为了两类,既然有弱引用,那么必然的,就会有强引用,所以我们得区分开这个强引用和弱引用。
强引用
使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚 拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时 间就会回收该对象。
弱引用
JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用 java.lang.ref.WeakReference 类来表示。可以在缓存中使用弱引用。
上面阿粉也说了,每一个 ThreadLocal 维护一个 ThreadLocalMap,key为使用 弱引用 的 ThreadLocal 实例,value为线程变量的副本。而他们之间的引用关系如下图:
在这个图中
- 实线表示-强引用
- 虚线表示-弱引用
从图中,我们就能分析出这个 ThreadLocal 是怎么出现内存泄露的了,我们从图中能够看到,ThreadLocalMap 使用 ThreadLocal 的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致 ThreadLocalMap 中 key 为null, 而 value 还存在着强引用,只有 Thead 线程退出以后, value 的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
这个时候,永远无法回收,就会造成 ThreadLocal 出现内存泄露的问题了。
这个时候就有读者会问,为什么 ThreaLocalMap 使用 ThreadLocal 的弱引用,而不去使用强引用,使用强引用的话,是不是就不会出现这个内存泄露的问题了呢?
其实这完全不是一回事,因为如果这时候 ThreaLocalMap 的 key 为强引用回收 ThreadLocal 时,因为 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致Entry内存泄漏。
这是使用强引用的时候,那么使用弱引用的时候是什么样的呢?
当 ThreadLocalMap 的 key 为弱引用回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
所以这么看下来,反而使用弱引用,却是更好地为什么呢?
因为使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次 ThreadLocalMap调用set(),get(),remove()的时候会被清除。
因此,ThreadLocal内存泄漏的根本原因是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
那么我们继续来看看这个 ThreadLocal 中维护的 ThreadLocalMap 的源码分析
static class ThreadLocalMap {
//hreadLocalMap中数据是存储在Entry类型数组的table中的,Entry继承了WeakReference(弱引用)
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
**成员变量**
//初始容量
private static final int INITIAL_CAPACITY = 16;
//ThreadLocalMap数据真正存储在table中
private Entry[] table;
//ThreadLocalMap条数
private int size = 0;
//达到这个大小,则扩容
private int threshold;
构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table数组,INITIAL_CAPACITY默认值为16
table = new Entry[INITIAL_CAPACITY];
//key和16取得哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建节点,设置key-value
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置扩容阈值
setThreshold(INITIAL_CAPACITY);
}
我们再来看看他的 remove 方法;
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//如果threadLocalHashCode计算出的下标找到的key和传入key不同,则证明出现哈希冲突,则循环向下查找
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果key相同
if (e.get() == key) {
//删除当前Entry
e.clear();
//清理
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal 中的 ThreadLocalMap 里面还有 set 和 getEntry 方法,还有很多,阿粉就不多介绍了。
那么我们应该如何合理的使用 ThreadLocal 才能保证内存不泄露呢?
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
原文 https://www.51cto.com/article/717119.html
猜你喜欢
- 2024-12-02 大厂面试必问:内存泄漏和内存溢出的区别?
- 2024-12-02 MyBatis居然也有并发问题(下:Hutool:WeakCache导致的内存泄漏
- 2024-12-02 深入理解Node.js中的垃圾回收和内存泄漏的捕获
- 2024-12-02 前端内存优化知多少?内存泄露只是冰山一角
你 发表评论:
欢迎- 07-08记oracle日志挖掘实操&查询归档不正常增长情况(一)
- 07-08Oracle 伪列!这些隐藏用法你都知道吗?
- 07-08orcl数据库查询重复数据及删除重复数据方法
- 07-08重大故障!业务核心表被truncate删除,准备跑路……
- 07-08oracle数据恢复—oracle执行truncate命令误删除数据的数据恢复
- 07-08Oracle-rac 修改scanip(oracle 修改sequence cache)
- 07-08ORACLE RAC CDB和PDB切换(oracle数据库rac切换)
- 07-08Oracle rac haip作用(oracle rac的典型特征)
- 596℃几个Oracle空值处理函数 oracle处理null值的函数
- 589℃Oracle分析函数之Lag和Lead()使用
- 576℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 573℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 569℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 562℃【数据统计分析】详解Oracle分组函数之CUBE
- 549℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 542℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
-
- 记oracle日志挖掘实操&查询归档不正常增长情况(一)
- Oracle 伪列!这些隐藏用法你都知道吗?
- orcl数据库查询重复数据及删除重复数据方法
- 重大故障!业务核心表被truncate删除,准备跑路……
- oracle数据恢复—oracle执行truncate命令误删除数据的数据恢复
- Oracle-rac 修改scanip(oracle 修改sequence cache)
- ORACLE RAC CDB和PDB切换(oracle数据库rac切换)
- Oracle rac haip作用(oracle rac的典型特征)
- 新手小白怎么学UI设计 推荐学习路线是什么
- 超实用!0基础UI设计自学指南(0基础学ui设计好就业吗)
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- 前端懒加载 (49)
- 前端获取当前时间 (50)
- 前端接口 (50)
- Oracle RAC (76)
- oracle恢复 (77)
- oracle 删除表 (52)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
本文暂时没有评论,来添加一个吧(●'◡'●)