并发编程的内存泄漏场景:隐藏的杀手💣

38 阅读3分钟

并发编程中的内存泄漏更隐蔽、更致命!ThreadLocal、线程池、监听器...处处是坑!

一、ThreadLocal导致的内存泄漏⚠️

问题代码

public class UserContext {
    private static ThreadLocal<User> userHolder = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userHolder.set(user);
    }
    
    public static User getUser() {
        return userHolder.get();
    }
    
    // ❌ 忘记清理!
}

// 在线程池中使用
executor.execute(() -> {
    UserContext.setUser(new User("张三"));
    doSomething();
    // 任务结束,但ThreadLocal没清理
    // 线程被复用,ThreadLocal中的User对象一直存在!
});

内存泄漏原理

Thread对象
  ↓ 强引用
ThreadLocalMap (threadLocals字段)
  ↓ Entry (弱引用key)
ThreadLocal对象 → null(被GC)
  ↓ 强引用value
User对象 → 无法GC!(泄漏!)

正确做法✅

public class UserContext {
    private static ThreadLocal<User> userHolder = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userHolder.set(user);
    }
    
    public static User getUser() {
        return userHolder.get();
    }
    
    // ✅ 清理方法
    public static void clear() {
        userHolder.remove();
    }
}

// 使用
executor.execute(() -> {
    try {
        UserContext.setUser(new User("张三"));
        doSomething();
    } finally {
        UserContext.clear(); // 一定要清理!
    }
});

二、线程池未关闭

问题代码

public class Service {
    public void process() {
        // ❌ 每次创建新线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);
        executor.execute(() -> {
            // 任务
        });
        // 没有shutdown,线程池一直存在!
    }
}

正确做法✅

public class Service {
    // ✅ 单例线程池
    private static final ExecutorService executor = 
        Executors.newFixedThreadPool(10);
    
    public void process() {
        executor.execute(() -> {
            // 任务
        });
    }
    
    // ✅ 应用关闭时
    public void shutdown() {
        executor.shutdown();
    }
}

三、监听器未移除

问题代码

public class EventBus {
    private List<EventListener> listeners = new CopyOnWriteArrayList<>();
    
    public void register(EventListener listener) {
        listeners.add(listener);
    }
    
    // ❌ 没有unregister方法
}

// 使用
EventListener listener = new HeavyListener(); // 大对象
eventBus.register(listener);
listener = null; // 想要GC
// 但listeners还持有引用,无法GC!

正确做法✅

public class EventBus {
    private List<EventListener> listeners = new CopyOnWriteArrayList<>();
    
    public void register(EventListener listener) {
        listeners.add(listener);
    }
    
    // ✅ 提供移除方法
    public void unregister(EventListener listener) {
        listeners.remove(listener);
    }
}

// 或使用弱引用
public class EventBus {
    private List<WeakReference<EventListener>> listeners = 
        new CopyOnWriteArrayList<>();
    
    public void register(EventListener listener) {
        listeners.add(new WeakReference<>(listener));
    }
}

四、静态集合持有对象

问题代码

public class Cache {
    // ❌ 静态集合,永远不会GC
    private static Map<String, byte[]> cache = new HashMap<>();
    
    public static void put(String key, byte[] data) {
        cache.put(key, data); // 数据越来越多
    }
}

正确做法✅

// 方案1:使用弱引用
private static Map<String, WeakReference<byte[]>> cache = 
    new WeakHashMap<>();

// 方案2:设置容量上限
private static Map<String, byte[]> cache = 
    new LinkedHashMap<String, byte[]>(100, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry eldest) {
            return size() > 100; // 超过100个移除最老的
        }
    };

// 方案3:使用Guava Cache(自动过期)
private static LoadingCache<String, byte[]> cache = 
    CacheBuilder.newBuilder()
        .maximumSize(1000)
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .build(loader);

五、内部类持有外部类引用

问题代码

public class Outer {
    private byte[] data = new byte[1024 * 1024]; // 1MB
    
    public void start() {
        // ❌ 非静态内部类持有Outer引用
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    // 长时间运行
                    // Outer对象无法GC
                }
            }
        }).start();
    }
}

正确做法✅

public class Outer {
    private byte[] data = new byte[1024 * 1024];
    
    public void start() {
        // ✅ 静态内部类或lambda
        new Thread(() -> {
            while (true) {
                // 不持有Outer引用
            }
        }).start();
    }
}

六、检测工具

1. MAT(Memory Analyzer Tool)

# 1. 生成heap dump
jmap -dump:live,format=b,file=heap.bin <pid>

# 2. 用MAT分析
# 查看Dominator Tree
# 查找泄漏对象

2. VisualVM

# 启动VisualVM
jvisualvm

# 连接应用
# 查看堆内存
# 生成heap dump

3. JProfiler

// 分析内存分配
// 查看对象引用链
// 实时监控内存

七、预防内存泄漏checklist✅

□ ThreadLocal用完调用remove()
□ 线程池应用关闭时shutdown()
□ 监听器提供unregister方法
□ 静态集合设置容量上限或过期时间
□ 避免非静态内部类持有外部引用
□ 定期检查堆内存
□ 使用WeakReference/SoftReference
□ 及时关闭资源(finally块)

八、面试高频问答💯

Q: ThreadLocal为什么会内存泄漏?

A: ThreadLocalMap的Entry的key是弱引用,value是强引用。key被GC后,value无法被访问但仍存在。

Q: 如何避免ThreadLocal泄漏?

A: 用完后调用remove()

Q: 为什么静态集合容易泄漏?

A: 静态变量生命周期等同于应用,集合中的对象永远不会GC。

Q: 如何检测内存泄漏?

A:

  • 生产:监控堆内存增长趋势
  • 排查:MAT分析heap dump
  • 预防:代码Review + 压测

下一篇→ 手写限流器:令牌桶、漏桶、滑动窗口🚰