# 5.4 内存泄露
用"水能载舟亦能覆舟"来形容用ThreadLocal的是十分贴切的,笔者在实际工作中遇到非常多的ThreadLocal问题, 如内存泄露、脏数据和线程上下文丢失等,特别是线程池场景,很容易因为使用不当导致线上事故。
# 5.4.1 内存泄露原因
ThreadLocal内存泄一般是如下原因造成:
- ThreadLocal变量没有被明确的移除
- ThreadLocal变量一直存在于ThreadLocalMap中
在使用ThreadLocal时,当线程结束,如果ThreadLocal变量没有被手动清除,就会导致这部分内存无法被回收,最终导致内存泄漏。
每个线程都有一个ThreadLocalMap,这个Map可以存放多个ThreadLocal变量。当ThreadLocal变量没有被移除时,它所引用的对象也会一直存放在线程的ThreadLocalMap中, 这会导致ThreadLocalMap变得很大,从而占用大量的内存空间,最终导致内存泄漏。
# 5.4.2 内存泄漏的检测与清除
一般的,在线程变量使用完成之后,应该立即调用remove()完成对变量的清除,并且最好将remove()方法放在finally块, 以确保一定能被执行到。如下所示:
ThreadLocal<Object> threadlocal = new ThreadLocal<>();
try {
Object value = new Object();
threadlocal.set(value);
// 业务逻辑...
} finally {
// 确保清除操作一定可以执行到
threadlocal.remove();
}
2
3
4
5
6
7
8
9
但是上面的方式仅适合非常简单的场景,复杂场景下如多个线程变量或者线程变量在多个地方使用等,将显得无力。 下面介绍开源中间件对线程变量的检测与清理。
# 5.4.3 tomcat中内存泄漏的检测
在前面的章节中,分析了tomcat在卸载war包的过程,在卸载war包时调用war的类加载器WebappClassLoaderBase的stop方法完成资源的关闭与清理操作。 其中就包括检测用户创建的线程变量是否得到了清除。来看下代码:
代码来源:apache-tomcat-10.1.13-src/java/org/apache/catalina/loader/WebappClassLoaderBase.java
private void checkThreadLocalsForLeaks() {
// 获取 jvm 全部线程
Thread[] threads = getThreads();
try {
// 反射获取threadLocals、inheritableThreadLocals
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals");
inheritableThreadLocalsField.setAccessible(true);
// 反射获取ThreadLocalMap的table字段
Class<?> tlmClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
// 反射获取expungeStaleEntries方法,该方法的作用是清除所有过期的entry
Method expungeStaleEntriesMethod = tlmClass.getDeclaredMethod("expungeStaleEntries");
expungeStaleEntriesMethod.setAccessible(true);
// 遍历所有线程,清除引用
for (Thread thread : threads) {
Object threadLocalMap;
if (thread != null) {
// 清除 threadLocalsField 字段引用的对象
threadLocalMap = threadLocalsField.get(thread);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
// 检测已经被完全清楚干净,如果发现entry的key或者value对象的类是由当前类的war包加载器加载
// 说明依然存在内存泄漏,需要进行修复。
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
// 清除 inheritableThreadLocalsField 字段引用的对象
threadLocalMap = inheritableThreadLocalsField.get(thread);
if (null != threadLocalMap) {
expungeStaleEntriesMethod.invoke(threadLocalMap);
checkThreadLocalMapForLeaks(threadLocalMap, tableField);
}
}
}
} catch (Throwable t) {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
上面的代码主要是遍历所有线程,然后分析每个线程的ThreadLocalMap的对象(包括threadLocals和inheritableThreadLocals),检测线程变量是否被清除。
需要说明的是,JDK17以上版本默认禁止跨包的反射操作,因此需要业务在jvm参数中增加--add-opens=java.base/java.lang=ALL-UNNAMED
解除限制。