Tomcat9基于javax的WebSocket服务出现Java heap space内存溢出问题
问题根因
- 广播逻辑冗余序列化:当前广播方法在遍历每个Session时都单独序列化一次消息对象,N个在线用户就会生成N份消息字符串副本,单条5M消息1000在线用户就会产生5G的临时字符串,直接占用大量堆内存。
- Session资源泄漏:
@OnError回调中仅关闭了Session,未从全局GlobalSessions列表中移除无效Session,无效 Session 会持续参与广播逻辑,产生大量无用的序列化字符串与发送任务。 - Tomcat WebSocket缓冲区不合理分配:你设置了
maxMessageSize=5M,Tomcat默认会为每个WebSocket Session分配和maxMessageSize大小一致的文本缓冲区,当在线Session数较多时,仅缓冲区就会占用「Session数 * 5M」的内存,这也是你把maxMessageSize降到1M后内存同步降到1/5的核心原因。 - 同步发送导致消息对象长时间被持有:使用
getBasicRemote()同步发送消息时,如果客户端网络卡顿,发送线程会阻塞,消息字符串的引用会被持续持有直到发送完成,大量并发发送时会导致消息对象无法被GC回收。 - 软引用回收策略问题:JDK8默认Parallel GC对软引用的回收策略是根据堆大小计算存活时间,8G堆的默认配置下软引用最多可存活2小时以上,Tomcat内部缓存的WebSocket缓冲区软引用会长时间占用内存。
解决方案
- 优化广播逻辑:提前将消息序列化为字符串,所有Session复用同一份序列化结果,避免N次冗余序列化,直接降低N倍的消息内存占用:
// 先把ObjectMapper改成全局单例 static ObjectMapper mapper = new ObjectMapper(); public void broadcastToSessions(Message messageObj) throws JsonProcessingException { // 提前序列化一次 String messageStr = mapper.writeValueAsString(messageObj); GlobalSessions.forEach(eachSession -> { try { if (eachSession.isOpen()) { synchronized(eachSession) { eachSession.getBasicRemote().sendText(messageStr); } } } catch (IOException e) { logger.error(e.getMessage(),e); } }); }
- 修复Session泄漏:在
@OnError回调中添加移除Session的逻辑,确保所有无效Session都会被从全局列表中清理:
@OnError public void onError(Session session, Throwable t) { try { GlobalSessions.remove(session); // 新增清理逻辑 session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, t.getMessage())); if(t instanceof IOException) { logger.error(t.getMessage()); }else { logger.error(t.getMessage(), t); } }catch (Exception e) { logger.error(e.getMessage(), e); } }
- 调整Tomcat WebSocket缓冲区配置:在Tomcat的
conf/context.xml中添加如下配置,单独设置缓冲区大小为常用消息大小(比如16K),避免缓冲区默认和maxMessageSize对齐:
<Context> <!-- 原有配置不变,新增以下两个参数 --> <Parameter name="org.apache.tomcat.websocket.textBufferSize" value="16384" /> <Parameter name="org.apache.tomcat.websocket.binaryBufferSize" value="16384" /> </Context>
- 替换为异步发送:使用
getAsyncRemote()替代同步的getBasicRemote(),避免发送线程阻塞,同时可以设置发送超时,减少消息对象的持有时间:
eachSession.getAsyncRemote().sendText(messageStr, result -> { if (!result.isOK()) { logger.error("消息发送失败", result.getException()); } });
- 优化GC配置:将GC替换为G1GC,同时调整软引用存活时间,在JAVA_OPTS中添加如下参数:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:SoftRefLRUPolicyMSPerMB=100
不要手动调用System.gc(),该操作会触发全量GC导致长时间停顿,反而影响服务可用性。
内容的提问来源于stack exchange,提问作者rahul
相关产品推荐
相关产品推荐

