# 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();
}
1
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) {
        // ...
     }
 }
1
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解除限制。