You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tomcat9基于javax的WebSocket服务出现Java heap space内存溢出问题

问题根因
  1. 广播逻辑冗余序列化:当前广播方法在遍历每个Session时都单独序列化一次消息对象,N个在线用户就会生成N份消息字符串副本,单条5M消息1000在线用户就会产生5G的临时字符串,直接占用大量堆内存。
  2. Session资源泄漏:@OnError回调中仅关闭了Session,未从全局GlobalSessions列表中移除无效Session,无效 Session 会持续参与广播逻辑,产生大量无用的序列化字符串与发送任务。
  3. Tomcat WebSocket缓冲区不合理分配:你设置了maxMessageSize=5M,Tomcat默认会为每个WebSocket Session分配和maxMessageSize大小一致的文本缓冲区,当在线Session数较多时,仅缓冲区就会占用「Session数 * 5M」的内存,这也是你把maxMessageSize降到1M后内存同步降到1/5的核心原因。
  4. 同步发送导致消息对象长时间被持有:使用getBasicRemote()同步发送消息时,如果客户端网络卡顿,发送线程会阻塞,消息字符串的引用会被持续持有直到发送完成,大量并发发送时会导致消息对象无法被GC回收。
  5. 软引用回收策略问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 02:15:02