websocket会话未清理是java后端内存泄漏典型场景,表现为standardwebsocketsession实例数持续增长、老年代内存只增不减,需通过jstat/jmap/arthas等工具确认现象、定位强引用源头并修正生命周期管理逻辑。

WebSocket 会话未清理是 Java 后端内存泄漏的典型场景,核心表现是:连接数不多,但 StandardWebSocketSession 或 DefaultHandshakeHandler 实例数持续增长,老年代内存只增不减。排查需分三步走——先确认现象,再定位强引用源头,最后修正生命周期管理逻辑。
确认是否真存在会话级泄漏
别急着改代码,先用工具验证:
- 运行
jstat -gc <pid></pid>,观察老年代(Old Used)是否随连接波动缓慢上升,且多次 Full GC 后不回落 - 执行
jmap -histo:live <pid> | grep "org.springframework.web.socket"</pid>,重点看StandardWebSocketSession实例数是否远超当前活跃连接数(如 50 个在线用户却有 800+ session) - 用
jmap -dump:format=b,file=heap.hprof <pid></pid>生成堆转储,用 VisualVM 或 MAT 打开 → “Group by Class” → 筛选WebSocketSession→ 查看其 **Retained Heap** 和 **incoming references**,确认哪些对象在阻止回收(常见:静态 Map、未注销监听器、ThreadLocal value)
检查 Session 存储与销毁逻辑
90% 的泄漏源于开发者手动维护 session 映射,却忽略清理契约:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 禁用
static ConcurrentHashMap<string websocketsession></string>直接缓存;若必须用,务必在afterConnectionClosed()中map.remove(session.getId()),且要处理网络闪断等异常断连场景 - 推荐改用
ConcurrentHashMap<string weakreference>></string>,或结合 Spring 事件驱动:@EventListener监听SessionDisconnectEvent触发清理 -
afterConnectionEstablished中存 session,afterConnectionClosed中删 session,二者必须严格配对;注意session.close()调用后禁止再调用sendMessage(),否则抛异常并可能卡住状态
排查隐式强引用陷阱
看似无关的代码,可能悄悄拖住整个 session:
- 自定义
HandshakeInterceptor中若将WebSocketSession存入ThreadLocal,必须在afterHandshake()里显式remove();否则ThreadLocal的 value 是强引用,线程复用时 session 就泄漏了 - 异步任务(如
@Async方法、定时任务)中若持有WebSocketSession引用,应改用session.getId()字符串,在执行时通过WebSocketSessionRegistry查询,避免长生命周期任务锁死 session - 检查消息处理器、自定义
SubProtocolHandler是否缓存了 session 关联的上下文对象(如用户权限、房间状态),这些对象若被静态容器或单例持有,也会间接导致 session 无法释放
用 Arthas 快速验证与热修复
无需重启,线上实时诊断:
- 启动 Arthas:
java -jar arthas-boot.jar,选择目标进程 - 查实例数:
vmtool --action getInstances --className org.springframework.web.socket.WebSocketSession --limit 10 - 监控关键方法:
monitor -c 5 com.yourpackage.MyWebSocketHandler afterConnectionClosed,确认该方法是否被正常调用 - 执行 OGNL 表达式查看静态 Map 内容:
ognl '#context = @org.springframework.web.context.ContextLoader@getCurrentWebApplicationContext(), #context.getBean("sessionRegistry").getSessions().size()'
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










