DocumentBuilder.parse() 易触发 OOM,因其将整个XML加载为DOM树,节点对象开销大、无法流式释放、存在XXE和字符串重复等问题;应改用SAX或StAX流式解析,并禁用DTD、复用对象、合理分块处理。

为什么 DocumentBuilder.parse() 容易触发 OOM
Java 中用 DocumentBuilder.parse(InputStream) 解析大 XML 文件时,DOM 会把整个文档树加载进内存。哪怕只有 100MB 的 XML,实际堆内存占用可能翻倍——因为每个 Element、Text、命名空间节点都带对象头、引用、字符数组副本。JVM 堆设得再大,也只是拖延 OOM 时间,不是解法。
- DOM 不支持流式释放:解析完即全量驻留,GC 无法在解析中途回收中间节点
- 默认
DocumentBuilderFactory未关闭外部实体(XXE),某些 DTD 加载行为会额外拉取远程资源并缓存 - 字符串重复:同一标签名、属性名在不同节点中被多次创建
String实例,加剧堆压力
改用 SAXParser 或 XMLStreamReader 的关键动作
二者都是事件驱动、只保留当前上下文,内存占用稳定在 KB 级别。选型看需求:SAX 更轻量但不可回溯;StAX(XMLStreamReader)支持部分前向移动,调试友好。
- 禁用 DTD 和外部实体:
System.setProperty("javax.xml.parsers.SAXParserFactory", "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"); SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); factory.setFeature("http://xml.org/sax/features/external-general-entities", false); - 不要在
startElement()里缓存全部文本内容——用StringBuilder拼接characters()回调,且每次处理完立即清空或复用 - 避免在 handler 中创建新对象:重用
Attributes参数,提取需要的属性值后立刻转成基本类型或字符串 intern
XmlMapper(Jackson Dataformat XML)解析大文件的陷阱
很多人以为 Jackson XML 是“流式”,其实 XmlMapper.readTree() 仍是 DOM 风格全量构建树,和原生 DOM 一样吃内存。真正安全的是 JsonParser + 手动遍历 token 流。
- 必须用
XmlFactory.createParser(InputStream)获取JsonParser,而非readValue() - 跳过无关字段用
parser.nextToken()+parser.getCurrentName()判断,不要调用parser.readValueAsTree() - 数值字段优先用
parser.getIntValue()、parser.getDecimalValue(),避免构造临时字符串 - 注意默认
XmlFactory允许 DTD:需显式设置factory.configure(FromXmlParser.Feature.USE_DTD, false)
真实场景下的分块与限流策略
即使用了流式解析,如果业务逻辑本身要聚合数据(比如按 分组统计),仍可能因缓存太多中间结果而 OOM。这时不能只依赖解析器,得配合业务切分。
- 用
CountingInputStream(Apache Commons IO)监控已读字节数,每 50MB 主动触发一次 flush + checkpoint - 对重复结构(如订单列表),用「解析即处理」模式:遇到
开始就 new 对象,结束就入库/发消息,不存 List - 若必须暂存,用
WeakHashMap或软引用缓存计算结果,避免强引用锁死内存 - 生产环境务必加 JVM 参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof,OOM 后直接看哪个类实例最多
最常被忽略的一点:XML 命名空间声明(xmlns)虽不占数据体积,但每个带 namespace 的元素都会触发 NamespaceContext 初始化和缓存——在百万级节点场景下,这部分对象能占到堆的 15% 以上。解析前先确认是否真需要 namespace 支持,不需要就关掉 factory.setNamespaceAware(false)。










