生产问题一则:ThreadLocal使用不当导致的内存泄露
Posted 水边2
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了生产问题一则:ThreadLocal使用不当导致的内存泄露相关的知识,希望对你有一定的参考价值。
先简单介绍一下 ThreadLocal,它是一个线程级的数据存储对象,生命周期是从一个线程的创建到该线程销毁。ThreadLocal里的数据,只能被所持有的线程读取,所以是线程安全的(前提是该数据不是多线程共享的)。
ThreadLocal的get、set、remove等方法,其实是对Thread.threadLocals
字段的封装,而Thread.threadLocals
的类型是ThreadLocalMap
,它维护了一个key/value键值对,
ThreadLocalMap.set
方法简略代码如下:
// 注意这是缩减版本,不是原始版本
private Entry[] table; // 数组数据,index为key的hash值
private int threshold;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = this.table;
int i = key.threadLocalHashCode & (tab.length-1); // 计算hash,存入数组指定位置
tab[i] = new Entry(key, value);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从代码可以看到,ThreadLocalMap
存储的数据Entry
有key和value,key是WeakReference
弱引用的数据,value就是我们往里面放的内容。
注:弱引用,就是在GC回收时,这个数据如果只有弱引用,没有强引用,依旧会被回收,下面贴一段弱引用被回收的测试代码:
@Test
public void gcTest() {
//Dto dto = new Dto(123, "abc"); // 不能用局部变量定义,会有强引用
WeakReference weak = new WeakReference<>(new Dto(123, "abc"));
Assert.notNull(weak.get(), "还没有GC,Dto对象存在,不为null");
System.gc(); // 强制GC回收
Assert.isNull(weak.get(), "Dto对象被回收了,所以是null");
}
由于ThreadLocalMap
的生命周期是线程销毁,在Web服务中通常使用的是线程池,所以基本上都不会销毁,数据无法自动清理(注:ThreadLocal的get、set、remove等方法会对数据的key为null的进行清理,set只是部分清理),从而导致泄露。
言归正传,昨天,群里收到系统内存达到80%的告警,因为高峰期,且系统能支撑,就只进行监控。 在高峰期过后,上去打了一个内存dump,命令如下:
sudo -u nobody jmap -dump:format=b,file=dump.hprof xxx
注:命令行里的xxx就是Java进程id,打dump可能出现的问题:
Error attaching to core file: cannot open binary file
这是要用进程启动用户执行命令,如 sudo -u nobodyError attaching to core file: debuggee is 32 bit, use 32 bit java for debugger
这是要用64位,如 jmap -J-d64
dump打好后,zip压缩再拖回来,启动Windows环境下,JDK里的 jvisualvm
命令,点菜单文件
->装入
,加载这个堆dump文件。
等加载完成后,点击界面右侧的:最大的对象->查找,会返回内存占用最大的20个对象,这个过程需要几分钟,比较慢,结果如下图:
从上图看到,在ThreadLocalMap里存储了42M的对象,随便点开一个java.lang.ThreadLocal$ThreadLocalMap$Entry#1335
,再依次展开该对象的属性,可以看到,在这个Map里存储了一堆的ChildRequestDto
对象,如图:
分析到这里,打开对应项目的代码,查找ChildRequestDto
类,以及关联的ThreadLocal
代码,定位到了问题所在:
@Around("controller()")
public Object doControllerAround(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
try {
JoinPointExexute joinPointExexute = new JoinPointExexute(pjp).invoke();
result = joinPointExexute.getResult();
} catch (Exception e) {
// 业务逻辑
throw e;
}
ApiRequestContext.clear(); // 清理ThreadLocal数据
return result;
}
上面的代码,在抛出异常的时候,会跳出方法,没有清理数据,从而导致的内存泄露的可能。
而且,这个代码,因为没有清理,也会导致数据泄露或篡改可能,比如:
- 张三登录系统,处理完业务后退出,他的线程放回线程池
- 李四登录系统,系统从线程池取出张三的线程为李四服务,这样代码处理不当时,李四就可能看到张三的数据,甚至修改张三的数据
解决方法就是把clear放在finally里了。
更好的做法,是把ThreadLocal的数据清理,放在HandlerInterceptor.afterCompletion
里,或者Filter里
进一步思考,在常规的业务代码里:
- 不推荐使用Aspect这样的aop能力,容易与框架层代码逻辑冲突,也影响业务问题的排查;
- 也不推荐使用ThreadLocal这样的上下文数据传输能力,影响问题排查,也影响单元测试。
- aop或ThreadLocal一般建议用在框架层,做一些底层支持和通用的业务支持,如果业务层需要传输数据,可以使用
HandlerMethodArgumentResolver
给Controller添加参数。
以上是关于生产问题一则:ThreadLocal使用不当导致的内存泄露的主要内容,如果未能解决你的问题,请参考以下文章
生产排查 | MySQL主从同步时报错1864之slave_pending_jobs_size_max设置不当导致的主从失效案例
Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析