qidao123.com技术社区-IT企服评测·应用市场

标题: 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个! [打印本页]

作者: 兜兜零元    时间: 2025-2-25 21:18
标题: 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
引子:那个让运维集体加班的夜晚

"凡哥!线上服务响应时间飙到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. }
复制代码
▌ 翻车现场
▌ 正确姿势
  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. }
复制代码
▌ 诡异现象
▌ 抢救方案
  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. }
复制代码
▌ 内存曲线
▌ 避坑绝招
  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. }
复制代码
▌ 灾难现场
▌ 正确配置
  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. }
复制代码
▌ 泄露证据
▌ 正确姿势
  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); // 只设置了数量,没设过期时间!
复制代码
▌ 内存症状
▌ 正确配置
  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. }
复制代码
▌ 内存异常
▌ 修复方案
  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分析三板斧
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
复制代码
防泄漏军规十二条

运维老凡的避坑日记
2024-03-20 凌晨2点
"小王啊,知道为什么我头发这么少吗?
当年有人把用户会话存到ThreadLocal里不清理,
结果线上十万用户同时在线时——
那内存泄漏的速度比剃头店推子还快!"
自测题:你能看出这段代码哪里会泄漏吗?
  1. // 危险代码!请找出三个泄漏点
  2. public class ModelLoader {
  3.     private static List<Model> loadedModels = new ArrayList<>();
  4.    
  5.     public void load(String path) {
  6.         Model model = new Model(Files.readAllBytes(Paths.get(path)));
  7.         loadedModels.add(model);
  8.         Executors.newSingleThreadScheduledExecutor()
  9.                  .scheduleAtFixedRate(() -> model.refresh(), 1, 1, HOURS);
  10.     }
  11. }
复制代码
答案揭晓

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/) Powered by Discuz! X3.4