"凡哥!线上服务响应时间飙到10秒了!"凌晨1点,实习生小李的语音带着哭腔。
监控大屏上,JVM堆内存曲线像坐了火箭——刚扩容的16G内存,30分钟就被吃干抹净。
我咬着牙拍桌子:"把最近一周上线的代码给我翻个底朝天!"
▌ 翻车代码(真实项目片段)
// 缓存用户AI对话历史 → 翻车写法!
public class ChatHistoryCache {
private static Map> cache = new HashMap();
public static void addMessage(Long userId, String msg) {
cache.computeIfAbsent(userId, k -> new ArrayList()).add(msg);
}
}
▌ 翻车现场
vmtool --action getInstances -c 4614556e
看到Map尺寸破千万HashMap$Node
对象占堆内存82%▌ 正确姿势
// 改用Guava带过期时间的缓存
private static Cache> cache = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.maximumSize(10000)
.build();
▌ 致命代码(处理AI模型文件)
// 加载本地模型文件 → 翻车写法!
public void loadModels(List files) {
files.forEach(file -> {
try {
InputStream is = new FileInputStream(file); // 漏了关闭!
parseModel(is);
} catch (IOException e) { /*...*/ }
});
}
▌ 诡异现象
lsof -p 进程ID | grep 'deleted'
发现大量未释放文件句柄jcmd PID VM.native_memory
显示文件描述符数量突破1万▌ 抢救方案
// 正确写法:try-with-resources自动关闭
files.forEach(file -> {
try (InputStream is = new FileInputStream(file)) { // 自动关流
parseModel(is);
} catch (IOException e) { /*...*/ }
});
▌ 坑爹代码(消息通知模块)
// 监听AI处理完成事件 → 翻车写法!
@Component
public class NotifyService {
@EventListener
public void handleAiEvent(AICompleteEvent event) {
// 错误持有外部服务引用
externalService.registerCallback(this::sendNotification);
}
}
▌ 内存曲线
NotifyService
实例数随时间线性增长▌ 避坑绝招
// 使用弱引用解除绑定
public void handleAiEvent(AICompleteEvent event) {
WeakReference 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()); // 无界队列!
}
▌ 灾难现场
byte[]
占内存90%,全是待处理的响应数据queue_size
指标持续高位不降▌ 正确配置
// 设置队列上限+拒绝策略
new ThreadPoolExecutor(10, 50,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
▌ 致命代码(查询用户对话记录)
public List getHistory(Long userId) {
SqlSession session = sqlSessionFactory.openSession();
try {
return session.selectList("queryHistory", userId);
} finally {
// 忘记session.close() → 连接池逐渐枯竭
}
}
▌ 泄露证据
Cannot get connection from pool, timeout 30000ms
SqlSession
实例数异常增长▌ 正确姿势
// 使用try-with-resources自动关闭
try (SqlSession session = sqlSessionFactory.openSession()) {
return session.selectList("queryHistory", userId);
}
▌ 问题代码(缓存用户偏好设置)
// 使用Ehcache时的错误配置
CacheConfiguration config = new CacheConfiguration()
.setName("user_prefs")
.setMaxEntriesLocalHeap(10000); // 只设置了数量,没设过期时间!
▌ 内存症状
watch com.example.CacheService getCachedUser
返回对象存活时间超7天UserPreference
对象▌ 正确配置
config.setTimeToLiveSeconds(3600) // 1小时过期
.setDiskExpiryThreadIntervalSeconds(60); // 过期检查间隔
▌ 致命代码(用户上下文传递)
public class UserContextHolder {
private static final ThreadLocal currentUser = new ThreadLocal();
public static void set(User user) {
currentUser.set(user);
}
// 缺少remove方法!
}
▌ 内存异常
User
对象被ThreadLocalMap
强引用无法释放▌ 修复方案
// 使用后必须清理!
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分析三板斧
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
# 统计对象数量排行榜
jmap -histo:live | head -n 20
# 强制触发Full GC(慎用!)
jcmd GC.run
try (InputStream is = ...) { // 第一重
useStream(is);
} catch (IOException e) { // 第二重
log.error("IO异常", e);
} finally { // 第三重
cleanupTempFiles();
}
运维老凡的避坑日记
2024-03-20 凌晨2点
"小王啊,知道为什么我头发这么少吗?
当年有人把用户会话存到ThreadLocal里不清理,
结果线上十万用户同时在线时——
那内存泄漏的速度比理发店推子还快!"
自测题:你能看出这段代码哪里会泄漏吗?
// 危险代码!请找出三个泄漏点
public class ModelLoader {
private static List 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);
}
}
答案揭晓:
本文来自博客园,作者:程序员晓凡,转载请注明原文链接:https://www.cnblogs.com/xiezhr/p/18737457
参与评论
手机查看
返回顶部