引子:那个让运维集体加班的夜晚
"凡哥!线上服务响应时间飙到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
复制代码 防泄漏军规十二条
- 所有缓存必须设置双保险:逾期时间 + 容量上限
- IO操纵三重防护:
- try (InputStream is = ...) { // 第一重
- useStream(is);
- } catch (IOException e) { // 第二重
- log.error("IO异常", e);
- } finally { // 第三重
- cleanupTempFiles();
- }
复制代码 - 线程池四不原则:
- 不消无界队列
- 不设不合理焦点数
- 不忽略拒绝策略
- 不存放大对象
- Spring组件三查:
- 查事件监听器引用链
- 查单例对象中的聚集类
- 查@Async注解的线程池配置
- 第三方库两验:
- 代码审查重点关注:
- 所有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);
- }
- }
复制代码 答案揭晓:
- static聚集无清理机制
- 定时使命线程池未关闭
- 匿名内部类持有Model强引用
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |