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

"凡哥!线上服务响应时间飙到10秒了!"凌晨1点,实习生小李的语音带着哭腔。

监控大屏上,JVM堆内存曲线像坐了火箭——刚扩容的16G内存,30分钟就被吃干抹净。

我咬着牙拍桌子:"把最近一周上线的代码给我翻个底朝天!"


第一坑:Static集合成永动机

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

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

▌ 翻车现场

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

▌ 正确姿势

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

第二坑:Lambda忘记关文件流

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

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

▌ 诡异现象

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

▌ 抢救方案

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

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

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

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

▌ 内存曲线

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

▌ 避坑绝招

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

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

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

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

▌ 灾难现场

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

▌ 正确配置

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

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

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

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

▌ 泄露证据

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

▌ 正确姿势

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

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

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

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

▌ 内存症状

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

▌ 正确配置

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

第七坑:ThreadLocal用完不打扫

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

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

▌ 内存异常

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

▌ 修复方案

// 使用后必须清理!
public static void remove() {
currentUser.remove();
} // 在拦截器中强制清理
@Around("execution(* com.example..*.*(..))")
public Object clearContext(ProceedingJoinPoint pjp) throws Throwable {
try {
return pjp.proceed();
} finally {
UserContextHolder.remove(); // 关键!
}
}

终极排查工具箱

1. Arthas实战三连击

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

2. MAT分析三板斧

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

3. 线上救火命令包

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

防泄漏军规十二条

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

运维老凡的避坑日记

2024-03-20 凌晨2点

"小王啊,知道为什么我头发这么少吗?

当年有人把用户会话存到ThreadLocal里不清理,

结果线上十万用户同时在线时——

那内存泄漏的速度比理发店推子还快!"


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

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

答案揭晓

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

凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!的更多相关文章

  1. java内存泄漏的几种情况

    转载于http://blog.csdn.net/wtt945482445/article/details/52483944 Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是静态 ...

  2. Java内存泄漏分析与解决方案

    Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历 ...

  3. Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析

    原文地址:http://www.javatang.com 一个典型的thread dump文件主要由一下几个部分组成: 上图将JVM上的线程堆栈信息和线程信息做了详细的拆解. 第一部分:Full th ...

  4. OutOfMemoryError异常java内存泄漏(Memory Leak)和内存溢出(Memory Overflow)

    本篇文章理解源自于<深入理解java虚拟机>2.4章节 实战:OutOfMemoryError异常   在以下例子中,所有代码都可以抛出OutOfMemoryError异常,但是要区分到底 ...

  5. java内存泄漏的定位与分析

    1.为什么会发生内存泄漏 java 如何检测内在泄漏呢?我们需要一些工具进行检测,并发现内存泄漏问题,不然很容易发生down机问题. 编写java程序最为方便的地方就是我们不需要管理内存的分配和释放, ...

  6. java内存泄漏

    java内存泄漏主要分成两个方面: (1)堆中申请的空间没有被释放 (2)对象已不在被使用,但是仍然存在在内存当中 以下集中情况可能会导致内存泄漏 (1)静态集合的使用hashmap和vector,静 ...

  7. (转)java内存泄漏的定位与分析

    转自:http://blog.csdn.net/x_i_y_u_e/article/details/51137492 1.为什么会发生内存泄漏 java 如何检测内在泄漏呢?我们需要一些工具进行检测, ...

  8. Java内存泄漏分析系列之五:常见的Thread Dump日志案例分析

    原文地址:http://www.javatang.com 症状及解决方案 下面列出几种常见的症状即对应的解决方案: CPU占用率很高,响应很慢 按照<Java内存泄漏分析系列之一:使用jstac ...

  9. Java内存泄漏分析系列之一:使用jstack定位线程堆栈信息

    原文地址:http://www.javatang.com 前一段时间上线的系统升级之后,出现了严重的高CPU的问题,于是开始了一系列的优化处理之中,现在将这个过程做成一个系列的文章. 基本概念 在对J ...

  10. JRockit检测Tomcat内存溢出JAVA内存泄漏问题

    http://blog.csdn.net/liyanhui1001/article/details/8240473 公司的一个Java应用系统上线以来,基本每1天OutOfMemoryError: P ...

随机推荐

  1. MQ系列 | RabbitMQ 消息确认机制

    RabbitMQ 消息确认机制 温馨提示:基于JDK17.SpringBoot 2.1.8.RELEASE 版本,由于RabbitMQ 在 SpringBoot3+ 的配置项有所不同, 所以请严格按照 ...

  2. 《Kubernetes故障篇:calico/node is not ready: BIRD is not ready》

    文章目录一.背景信息二.解决方法总结:整理不易,如果对你有帮助,可否点赞关注一下? 一.背景信息k8s集群部署后发现calico的pod未通过健康检查,如下所示: 通过命令kubectl descri ...

  3. [转]vue项目启动时,npm run serve 和 npm run dev 的区别

    npm run serve 和 npm run dev 的区别 在我们运行一些 vue 项目的时候,输入npm run serve或者 npm run dev的其中一个时,系统会报错: PS C:\U ...

  4. 即时通讯技术文集(第16期):IM架构设计技术精选(第一部分) [共17篇]

    为了更好地分类阅读总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第16 期. [- 1 -] 浅谈IM系统的架构设计 [链接] http://www.52im.net/thread ...

  5. [开源项目]YOLOv8_Efficient

    Yolov8_Efficient Simple and efficient use for yolov8 About This is an unofficial repository maintain ...

  6. 解决mapper重名问题

    问题 公司有一个集成开发平台,导入数据库表会自动生成实体类.mapper和xml等文件,这是一件很方便的事,可以省去很多没有技术性的重复工作. 但是最近我在使用这个平台的时候遇到了一个问题,那就是ma ...

  7. Windows安全加固(四)

    七.服务安全 1.禁用TCP/IP上的NetBIOS(协议所用端口139) 作用:禁用TCP/IP上的NetBIOS协议,可以关闭监听的UDP137.UDP138.UDP139端口. (1)使用快捷键 ...

  8. 性能测试工具_nGrinder

    1. ngrinder-controller-3.4.3.war 放置到tomcat的webapps目录下:2. 启动tomcat;3. 访问地址: http://localhost:8080/ngr ...

  9. Index - 此处的诗

    虚构往事 正篇   嗯--本来发过两篇,但深愧于仓促的处理和并未完善的细节设定,隐藏了.   大概会是一个中篇的科幻故事,世界设定已经完善了(Shaya 可以作证!),但近期可能没有精力动笔. 番外 ...

  10. 谈谈flutter的线程

    本文同步发布于公众号:移动开发那些事谈谈flutter的线程 刚接触flutter的同学肯定会对fluter所谓的单线程架构很蒙逼,因为这与我们学开发时,各种语言里的多线程的介绍有点出入,而且手机的C ...