经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
来源:cnblogs  作者:程序员晓凡  时间:2025/3/7 9:16:42  对本文有异议

引子:那个让运维集体加班的夜晚

"凡哥!线上服务响应时间飙到10秒了!"凌晨1点,实习生小李的语音带着哭腔。
监控大屏上,JVM堆内存曲线像坐了火箭——刚扩容的16G内存,30分钟就被吃干抹净。
我咬着牙拍桌子:"把最近一周上线的代码给我翻个底朝天!"


第一坑:Static集合成永动机

▌ 翻车代码(真实项目片段)

  1. // 缓存用户AI对话历史 → 翻车写法!
  2. public class ChatHistoryCache {
  3. private static Map<Long, List<String>> cache = new HashMap<>();
  4. public static void addMessage(Long userId, String msg) {
  5. cache.computeIfAbsent(userId, k -> new ArrayList<>()).add(msg);
  6. }
  7. }

▌ 翻车现场

  • 用户量暴增时,缓存数据只进不出,48小时撑爆内存
  • 用Arthas抓现行:vmtool --action getInstances -c 4614556e 看到Map尺寸破千万
  • MAT分析:HashMap$Node对象占堆内存82%

▌ 正确姿势

  1. // 改用Guava带过期时间的缓存
  2. private static Cache<Long, List<String>> cache = CacheBuilder.newBuilder()
  3. .expireAfterAccess(1, TimeUnit.HOURS)
  4. .maximumSize(10000)
  5. .build();

第二坑:Lambda忘记关文件流

▌ 致命代码(处理AI模型文件)

  1. // 加载本地模型文件 → 翻车写法!
  2. public void loadModels(List<File> files) {
  3. files.forEach(file -> {
  4. try {
  5. InputStream is = new FileInputStream(file); // 漏了关闭!
  6. parseModel(is);
  7. } catch (IOException e) { /*...*/ }
  8. });
  9. }

▌ 诡异现象

  • 服务运行三天后突然报 "Too many open files"
  • Linux排查:lsof -p 进程ID | grep 'deleted' 发现大量未释放文件句柄
  • JVM监控:jcmd PID VM.native_memory显示文件描述符数量突破1万

▌ 抢救方案

  1. // 正确写法:try-with-resources自动关闭
  2. files.forEach(file -> {
  3. try (InputStream is = new FileInputStream(file)) { // 自动关流
  4. parseModel(is);
  5. } catch (IOException e) { /*...*/ }
  6. });

第三坑:Spring事件监听成钉子户

▌ 坑爹代码(消息通知模块)

  1. // 监听AI处理完成事件 → 翻车写法!
  2. @Component
  3. public class NotifyService {
  4. @EventListener
  5. public void handleAiEvent(AICompleteEvent event) {
  6. // 错误持有外部服务引用
  7. externalService.registerCallback(this::sendNotification);
  8. }
  9. }

▌ 内存曲线

  • 每次事件触发,监听器对象就被外部服务强引用,永远不释放
  • MAT分析:NotifyService实例数随时间线性增长
  • GC日志:老年代占用率每周增长5%

▌ 避坑绝招

  1. // 使用弱引用解除绑定
  2. public void handleAiEvent(AICompleteEvent event) {
  3. WeakReference<NotifyService> weakRef = new WeakReference<>(this);
  4. externalService.registerCallback(() -> {
  5. NotifyService service = weakRef.get();
  6. if (service != null) service.sendNotification();
  7. });
  8. }

第四坑:线程池里的僵尸任务

▌ 问题代码(异步处理AI请求)

  1. // 异步线程池配置 → 翻车写法!
  2. @Bean
  3. public Executor asyncExecutor() {
  4. return new ThreadPoolExecutor(10, 10,
  5. 0L, TimeUnit.MILLISECONDS,
  6. new LinkedBlockingQueue<>()); // 无界队列!
  7. }

▌ 灾难现场

  • 请求突增时队列堆积50万任务,每个任务持有一个AI响应对象
  • 堆dump显示:byte[]占内存90%,全是待处理的响应数据
  • 监控指标:queue_size指标持续高位不降

▌ 正确配置

  1. // 设置队列上限+拒绝策略
  2. new ThreadPoolExecutor(10, 50,
  3. 60L, TimeUnit.SECONDS,
  4. new ArrayBlockingQueue<>(1000),
  5. new ThreadPoolExecutor.CallerRunsPolicy());

第五坑:MyBatis连接池里的幽灵

▌ 致命代码(查询用户对话记录)

  1. public List<ChatRecord> getHistory(Long userId) {
  2. SqlSession session = sqlSessionFactory.openSession();
  3. try {
  4. return session.selectList("queryHistory", userId);
  5. } finally {
  6. // 忘记session.close() → 连接池逐渐枯竭
  7. }
  8. }

▌ 泄露证据

  • Druid监控面板显示活跃连接数达到最大值
  • 日志报错:Cannot get connection from pool, timeout 30000ms
  • 堆分析:SqlSession实例数异常增长

▌ 正确姿势

  1. // 使用try-with-resources自动关闭
  2. try (SqlSession session = sqlSessionFactory.openSession()) {
  3. return session.selectList("queryHistory", userId);
  4. }

第六坑:第三方库的温柔陷阱

▌ 问题代码(缓存用户偏好设置)

  1. // 使用Ehcache时的错误配置
  2. CacheConfiguration<Long, UserPreference> config = new CacheConfiguration<>()
  3. .setName("user_prefs")
  4. .setMaxEntriesLocalHeap(10000); // 只设置了数量,没设过期时间!

▌ 内存症状

  • GC日志显示老年代每周增长3%
  • Arthas监控:watch com.example.CacheService getCachedUser返回对象存活时间超7天
  • 压测时触发OOM,堆中发现大量UserPreference对象

▌ 正确配置

  1. config.setTimeToLiveSeconds(3600) // 1小时过期
  2. .setDiskExpiryThreadIntervalSeconds(60); // 过期检查间隔

第七坑:ThreadLocal用完不打扫

▌ 致命代码(用户上下文传递)

  1. public class UserContextHolder {
  2. private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
  3. public static void set(User user) {
  4. currentUser.set(user);
  5. }
  6. // 缺少remove方法!
  7. }

▌ 内存异常

  • 线程池复用后,ThreadLocal中旧用户数据堆积
  • MAT分析:User对象被ThreadLocalMap强引用无法释放
  • 监控发现:每个线程持有平均50个过期用户对象

▌ 修复方案

  1. // 使用后必须清理!
  2. public static void remove() {
  3. currentUser.remove();
  4. }
  5. // 在拦截器中强制清理
  6. @Around("execution(* com.example..*.*(..))")
  7. public Object clearContext(ProceedingJoinPoint pjp) throws Throwable {
  8. try {
  9. return pjp.proceed();
  10. } finally {
  11. UserContextHolder.remove(); // 关键!
  12. }
  13. }

终极排查工具箱

1. Arthas实战三连击

  1. # 实时监控GC情况
  2. dashboard -n 5 -i 2000
  3. # 追踪可疑方法调用频次
  4. trace com.example.CacheService addCacheEntry -n 10
  5. # 动态修改日志级别(无需重启)
  6. logger --name ROOT --level debug

2. MAT分析三板斧

  • Dominator Tree:揪出内存吞噬者
  • Path to GC Roots:顺藤摸瓜找凶手
  • OQL黑科技
    1. SELECT * FROM java.util.HashMap WHERE size > 10000
    2. SELECT toString(msg) FROM java.lang.String WHERE msg.value LIKE "%OOM%"

3. 线上救火命令包

  1. # 快速查看堆内存分布
  2. jhsdb jmap --heap --pid <PID>
  3. # 统计对象数量排行榜
  4. jmap -histo:live <PID> | head -n 20
  5. # 强制触发Full GC(慎用!)
  6. jcmd <PID> GC.run

防泄漏军规十二条

  1. 所有缓存必须设置双保险:过期时间 + 容量上限
  2. IO操作三重防护
    1. try (InputStream is = ...) { // 第一重
    2. useStream(is);
    3. } catch (IOException e) { // 第二重
    4. log.error("IO异常", e);
    5. } finally { // 第三重
    6. cleanupTempFiles();
    7. }
  3. 线程池四不原则
    • 不用无界队列
    • 不设不合理核心数
    • 不忽略拒绝策略
    • 不存放大对象
  4. Spring组件三查
    • 查事件监听器引用链
    • 查单例对象中的集合类
    • 查@Async注解的线程池配置
  5. 第三方库两验
    • 验连接池归还机制
    • 验缓存默认配置
  6. 代码审查重点关注
    • 所有static修饰的集合
    • 所有close()/release()调用点
    • 所有内部类持有外部引用的地方

运维老凡的避坑日记

2024-03-20 凌晨2点
"小王啊,知道为什么我头发这么少吗?
当年有人把用户会话存到ThreadLocal里不清理,
结果线上十万用户同时在线时——
那内存泄漏的速度比理发店推子还快!"


自测题:你能看出这段代码哪里会泄漏吗?

  1. // 危险代码!请找出三个泄漏点
  2. public class ModelLoader {
  3. private static List<Model> loadedModels = new ArrayList<>();
  4. public void load(String path) {
  5. Model model = new Model(Files.readAllBytes(Paths.get(path)));
  6. loadedModels.add(model);
  7. Executors.newSingleThreadScheduledExecutor()
  8. .scheduleAtFixedRate(() -> model.refresh(), 1, 1, HOURS);
  9. }
  10. }

答案揭晓

  1. static集合无清理机制
  2. 定时任务线程池未关闭
  3. 匿名内部类持有Model强引用

原文链接:https://www.cnblogs.com/xiezhr/p/18737457

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号