心跳检测易致内存泄漏,关键在发送主体、方式及收尾:线程需用池化管理而非每连接新建;sse/ws连接关闭时须显式释放资源;避免静态缓存心跳数据;心跳消息应精简字段并复用对象以减少gc压力。

心跳检测本身不消耗多少内存,但实现方式不对,就容易变成内存泄漏的“帮凶”。关键不是心跳要不要发,而是谁在发、怎么发、发完有没有收尾。
检查心跳线程是否随连接数无限增长
这是最常见也最危险的问题。比如用 new Thread() 或 while(true) + sleep 为每个客户端单独启一个心跳线程,连接数一多,线程和对应对象就堆满堆内存。
- 查代码里是否有类似
new Thread(() -> { while(true) { ... } }).start()的写法 - 用
jstack或Arthas thread命令看线程数量是否与活跃连接数成正比 - 优先改用线程池调度,例如 Spring Boot 中用
ScheduledThreadPoolExecutor统一管理所有心跳任务
确认 SseEmitter / WebSocket Session 是否被正确释放
服务端维持长连接时,常把心跳逻辑和连接生命周期耦合在一起。客户端断开后,如果没显式调用 complete()(SSE)或 session.close()(WebSocket),关联的缓冲区、监听器、上下文对象就一直卡在内存里。
- 检查
@OnClose或onDisconnect回调中是否执行了资源清理 - 特别注意异步操作(如日志记录、MQ推送)是否持有对 session/emitter 的强引用
- 可在关闭前打印
emitter.getTimeout()和emitter.isCompleted()状态辅助判断
排查心跳消息携带的上下文对象是否被意外缓存
有些心跳实现会附带时间戳、设备ID、随机噪声等字段,若这些数据被塞进静态 Map、Guava Cache 或未设过期策略的 ConcurrentHashMap,就会越积越多。
- 搜索代码中是否有
static Map, ?>或CacheBuilder.newBuilder().build()被用于存储心跳相关键值 - 检查心跳响应体是否序列化了大对象(如完整用户信息、配置快照)
- 建议心跳只传最小必要字段,如
{"type":"hb","t":1726450260},避免隐式引用
验证心跳机制是否触发 GC 障碍
高频心跳(如每秒一次)若伴随频繁对象创建(如每次 new Date()、new JSONObject()),会加剧年轻代分配压力,导致 Minor GC 频繁,间接暴露内存回收问题。
- 用
jstat -gc <pid></pid>观察YGC次数和EU(Eden 使用量)变化趋势 - 心跳中尽量复用对象:使用
System.nanoTime()替代new Date(),用 StringBuilder 拼接 JSON 字符串 - 在心跳逻辑外加一层轻量级守护线程,定期 dump 当前活跃连接数与堆内存使用率,建立基线对比










