¶Spring AI MCP服务内存泄漏排查实录:从堆分析到源码修复
Spring AI构建的MCP服务频繁OOM?本文完整记录问题排查全链路:
1️⃣ 通过MAT精准定位WebMvcSseServerTransport中未释放的会话占99.59%内存
2️⃣ 发现SDK 0.7.0版本仅在异常时清理会话的设计缺陷
3️⃣ 升级1.0.0版本后仍存在异步连接残留问题
4️⃣ 最后采用"心跳检测+异常熔断"双保险机制
👉 关键方案:定时发送轻量级消息sendNotification,实现自动回收失效连接,彻底解决内存泄漏。附完整堆分析截图、源码对比!
上一篇文章我们介绍了如何使用Spring AI快速构建一个MCP Server:Spring AI+MCP实战:零代码改造将传统服务接入大模型生态,但是服务启动一段时间后,总是是内存溢出,导致MCP服务时不时就不可用,必须得重启才能解决。
配置java参数当内存溢出时自动转储堆,然后分析堆内存,终于发现了罪魁祸首,接下来就让我们一起来看看罪魁祸首是谁。
¶分析堆内存
使用MAT(Eclipse Memory Analyzer)打开自动转储的堆文件,加载完成后打开Leak Suspects可以发现内存泄露的可疑点:
从上图可以发现,由io.modelcontextprotocol.server.transport.WebMvcSseServerTransport @ 0x700730098对象持有的java.util.concurrent.ConcurrentHashMap$Node[]占用了99.59%的内存。
到这里基本就可以确定内存泄露的罪魁祸首就是WebMvcSseServerTransport,具体是其中的哪个对象呢,让我们继续分析。
点击Eclipse Memory Analyzer上的dominator_tree可以看到堆内存中对象的树形结构信息,这里根据Retained Heap降序排列,可以看到占用内存最多的对象java.util.concurrent.ConcurrentHashMap$Node[],右键 -》Path To GC Roots -》with all references,可以看到泄露对象到gc roots的路径,可以清晰的看到是被谁持有但一直未释放。
到这里我们知道了是WebMvcSseServerTransport#sessions属性持有了有大量的Map节点,但一直没释放,最终导致JVM内存溢出了。
MAT工具的文档详见文末的参考链接
¶源码分析
之前2025年3月份根据官方文档: https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html集成的时候,引入starter为:
1 | <dependency> |
其中引入的io.modelcontextprotocol.sdk:mcp-spring-webmvc的版本为0.7.0,WebMvcSseServerTransport的**核心实现(省略部分与本次内存溢出问题无关的代码)**如下:
1 | public class WebMvcSseServerTransport implements ServerMcpTransport { |
从上面代码可以发现,在MCP的ClientSession关闭时,只是将sseBuilder设置为完成;仅当handleSseConnection中出现异常时才会将ClientSession从sessions中移除,估计是想客户端一直复用这个连接吧。
那么正常情况下,这个session就会一直存在于WebMvcSseServerTransport#sessions属性中,而WebMvcSseServerTransport对象在MCP服务运行时会一直存活,因此一段时间后MCP服务就会因为WebMvcSseServerTransport#sessions属性内存泄露最终导致jvm的内存溢出。
¶SDK升级
经过上面的源码分析,我们知道了内存泄露的具体原因是WebMvcSseServerTransport#sessions的ClientSession一直在增长,因此只需要在ClientSession完成或异常的时候将其从sessions中移除即可。
经查看最新的官方文档: https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html,其中对于MCP Server Boot Starter已经做了升级,升级到1.0.0版本后,可以看到最新的版本是由WebMvcSseServerTransportProvider来管理see请求的,处理sse请求的源码如下:
1 | private ServerResponse handleSseConnection(ServerRequest request) { |
其中可以看到,在请求完成、超时和异常情况下都会将session移除,这样应该就能解决内存溢出问题了。
问题到这里真的解决了吗?
¶解决方案
经过验证,发现升级后的SDK里面WebMvcSseServerTransport#sessions中存放的McpServerSession还是会一直存在,并没有移除。
可能是异步请求的请求,Cursor之类的客户端在创建MCP连接后,即使Cursor关闭后也没有主动去告诉服务端断开连接,也就是不会触发onComplete、onTimeout方法去将session移除。
于是,我们可以定时去检测WebMvcSseServerTransport#sessions中的McpServerSession是否还存活,如果客户端已经把连接关闭了,那么就将session移除。
1 | /** |
发送检测消息后session.sendNotification("getCurrentTime"),Tomcat中间件会检测到该sse连接是否还存活,如果连接已断开会有如下异常信息(省略部分堆栈):
1 | 2025-07-02 14:01:31.064 [http-nio-8089-exec-13] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] threw exception |
Tomcat中间件在org.apache.catalina.core.AsyncContextImpl#doInternalDispatch中检测到异常后,会将连接设置为完成:
1 | protected void doInternalDispatch() throws ServletException, IOException { |
连接设置为完成后,最终会触发WebMvcSseServerTransport#handleSseConnection方法中的sseBuilder.onComplete回调中将session进行移除
1 | sseBuilder.onComplete(() -> { |
检测的日志输出情况如下:
1 | 2025-07-02 14:01:31.053 [scheduling-1] INFO c.t.smd.mcp.config.McpSessionConfig - 检测到 15 个活跃sessions,开始进行存活检测 |
到这里终于完美解决MCP服务因为连接泄露导致的内存溢出问题。
参考链接:
- https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html
- https://help.eclipse.org/latest/index.jsp?topic=/org.eclipse.mat.ui.help/welcome.html