JAX-WS-RI(Metro)大附件场景下堆内存占用过高问题
这个问题我之前帮团队排查过类似的,JAX-WS-RI在处理大附件时确实容易踩内存的坑,尤其是默认配置下很容易把整个消息“啃”进内存里。结合你描述的情况——50MB附件占1.5GB堆、堆转储全是String对象,还有你已经试过的MTOM、流式、分块方案,咱们来一步步定位解决:
首先先明确你的核心问题:
我们使用JAX-WS-RI从SOAP服务下载文档时遇到了严重的堆内存占用问题:下载50MB附件时JVM需占用1.5GB堆内存,远超预期。已尝试MTOM请求/响应、JAX-WS-RI流式处理(已生成临时文件)、HTTP分块传输,但均无改善。设置最大堆内存为1GB时出现OutOfMemoryError,堆转储显示JAX-WS、SAAJ创建了数百万个String对象。
为什么会出现这种情况?
大概率是SAAJ(SOAP with Attachments API for Java)的默认内存缓存在搞鬼,哪怕你开了MTOM或者流式,SAAJ可能还是会把整个SOAP消息(包括附件的Base64编码内容)加载到内存里;另外如果MTOM配置没真正生效,附件会被转成Base64字符串,这不仅会让体积膨胀30%,还会生成大量String对象占满堆。
具体解决方案
1. 先给SAAJ的内存缓存“断电”
SAAJ默认会缓存所有SOAP内容到内存,这是内存暴涨的重灾区,必须禁用:
- 方式一:启动时加系统属性
如果你用Oracle JDK,加:com.sun.xml.internal.messaging.saaj.disableMimeCache=true
如果是OpenJDK,加:com.sun.xml.messaging.saaj.disableMimeCache=true - 方式二:代码里直接配置
// 获取SOAP消息工厂并禁用MIME缓存 SOAPMessageFactory factory = SOAPMessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL); ((com.sun.xml.messaging.saaj.meta.SAAJMetaFactoryImpl) com.sun.xml.messaging.saaj.meta.MetaFactory.getInstance()) .setFeature(com.sun.xml.messaging.saaj.util.SAAJFeatureKeys.DISABLE_MIME_CACHE, true);
2. 把流式处理的配置拉满,别让内容进内存
你说已经用了流式,但可能没配置到位,得确保JAX-WS真的把附件写到临时文件,而不是偷偷加载到内存:
- 先给服务接口加
@StreamingAttachment注解,强制流式处理:@WebService @StreamingAttachment(parseEagerly = false, memoryThreshold = 0) // 0表示所有附件直接走临时文件 public interface DocumentService { @WebMethod DownloadResponse downloadDocument(@WebParam(name = "documentId") String documentId); } - 处理响应时,直接用
StreamingDataHandler写入文件,绝对不要转成byte[]或者String:DownloadResponse response = port.downloadDocument("你的文档ID"); StreamingDataHandler dataHandler = response.getAttachment(); try (OutputStream out = new FileOutputStream("下载的文件路径")) { dataHandler.writeTo(out); } finally { dataHandler.close(); // 一定要关闭,释放临时文件资源 }
3. 给JAX-WS设置附件内存阈值
让超过阈值的附件直接落地到临时文件,避免占用堆内存:
- 方式一:系统属性设置(比如设1MB,超过就写临时文件)
com.sun.xml.ws.attachment.memoryThreshold=1048576 - 方式二:代码里通过BindingProvider配置
BindingProvider bp = (BindingProvider) port; bp.getRequestContext().put(com.sun.xml.ws.developer.JAXWSProperties.ATTACHMENT_MEMORY_THRESHOLD, 1048576);
4. 检查你的拦截器有没有“拖后腿”
如果项目里加了SOAP拦截器(比如日志拦截器),一定要确保它不会把整个消息加载到内存里——比如别调用getMessage().getSOAPBody().toString()这种会触发全消息加载的方法,只读取必要的头部信息就行,别碰附件内容。
5. 再确认MTOM是真的生效了
如果MTOM没生效,附件会被转成Base64字符串塞进SOAP消息里,这会直接导致大量String对象出现。你可以:
- 客户端明确启用MTOM:
MTOMFeature mtomFeature = new MTOMFeature(); DocumentService port = new DocumentServiceService().getDocumentServicePort(mtomFeature); - 抓包验证:用Wireshark看响应的Content-Type是不是
multipart/related,如果是就说明MTOM生效了;如果还是text/xml或者application/soap+xml,那就是MTOM没配置对。
最后总结
按上面的步骤配置完,堆内存占用应该会降到合理水平(比如50MB附件占用几百MB以内)。你堆转储里的大量String对象,大概率是MTOM没生效导致的Base64编码字符串,或者SAAJ缓存了整个消息内容。先禁用SAAJ缓存,再把流式和MTOM配置到位,应该就能解决OOM的问题了。
内容的提问来源于stack exchange,提问作者Beatz

