Aspose.word 内存泄漏,依然存在WORDSJAVA-3167问题

大量输出文档,内容中有不同的SVG,SVG中有@font-face属性,内存一直升高直到溢出。
Aspose.Words 26.5版本依然存在WORDSJAVA-3167问题。
请尽快修复,已经严重影响用户正常使用!

@dhzhao2016 请您附上问题文档,并提供一段简单的代码,以便我们重现该问题?

html svg code.zip (36.1 KB)

请查看附件,html中带svg的内容,code是代码。像这种svg内容,大量写入文档时,或者循环多次,写入不同的文档时,内存溢出。测试输出1000个及以上文档时内存升高明显。

@dhzhao2016 我尝试在循环中生成相同的文档,但没有发现内存问题。您是否多次将相同的 HTML 代码插入到同一个文档中?如果是这样,文档大小预计会不断增加,因此文档 DOM 所需的内存也会相应增加。能否请您提供一些代码,以便我们能够重现内存不足的问题?

没有多次将相同的HTML代码插入到同一个文档。代码也没有什么特殊的操作,你就循环输出文档测试就能验证出来。
上面附件中的svg,都是各个试题的内容。我们的场景是学生考试后会将所有学生的试题输出到word或者pdf,比如有2000个学生,那么就是这么多的试题,分别输出到2000个文档中,在生产环境,大概输出600–800份时,内存就快达到10G了。

经AI分析,可能的原因有:

原因1:Aspose.Words 全局字体缓存(最可能,占80%以上)
现象 :带公式的答卷容易OOM,纯文本没问题 → 公式SVG中嵌入了字体数据!
原理 :

  1. 公式SVG中通常嵌入了 @font-face + base64编码的字体数据(stix、math1等20多种字体)
  2. 每次调用 builder.insertHtml(svg) 时,Aspose.Words会把这些字体加载到**全局静态字体缓存中
  3. Document.clearCaches() 只能清理Document级别的缓存,**清理不了全局静态的字体缓存
  4. 500份答卷 × 每份上百个公式 × 每个公式20多种字体 → 字体缓存无限增长

原因2:SVG转换后的Shape对象缓存未完全释放

  1. SVG被Aspose渲染为Shape对象后,内部可能有静态缓存
  2. Document.clearCaches() 可能不彻底

@dhzhao2016 感谢您提供的更多信息。我们在两台不同的机器上测试了该场景,仍然无法重现内存异常的输出。我们观察到,文档生成后内存已正确释放,内存使用情况稳定,没有增长。我们也检查了字体资源的处理方式,没有发现任何内存问题。

LeakRealityTest.zip (4.9 KB)

我没有办法提供你们生产环境的完整代码,只能尽可能的模拟内存溢出的情况,在本地环境也确实不好复现内存问题,请多试几次,或者多运行一段时间看看。
内存溢出的情况确实是存在的,请重视!特别是公式(SVG)内容较多的情况下比较明显。

@dhzhao2016 我们会进行调查,并尽快回复您。

还有一点,在生成学生答卷文件过程中,使用过文档的deepClone(),完整深拷贝复制所有内部结构,全局 FontStore ,包含主题字体,完整 SVG 渲染 ,含 @font-face 解析。
deepClone()的文档,最后也进行了cleanup、clearCaches清理,不知道对堆内存影响如何。

@dhzhao2016 感谢您提供的信息。

我们测试了多种场景。驻留堆内存在第 100 份文档时达到 84 MB,随后一直到第 600 份都精确保持在 84 MB,其余 500 份文档没有任何增长。我们还在完全去掉 cleanup()clearCaches() 的情况下运行了测试,结果完全相同 —— 这说明克隆出的文档并没有驻留任何"cleanup 未能释放"的内容。

遗憾的是,我们无法在隔离的 Aspose 代码路径中复现这种增长,这表明内存驻留发生在流程的其他环节。为了定位它,从您的环境中获取一份堆转储(Heap Dump)是最直接的方式。

请在约 600 份文档、OOM 发生之前抓取:

jcmd <pid> GC.heap_dump /path/to/heap_600.hprof

您也可以选择使用 jmap -dump:live,format=b,file=/path/to/heap_600.hprof <pid>,它生成的文件更小 —— 因为会先执行一次 Full GC,仅转储可达对象,而这正是我们需要的内容。

请在同一时刻同时执行以下两条命令,并将输出以纯文本形式发给我们。它们仅有几 KB,且不包含任何文档内容或学生数据:

jcmd <pid> GC.heap_info   # Java 堆的实际占用
jcmd <pid> VM.flags       # 实际生效的 JVM 参数(-Xmx、垃圾回收器等)

如果是 Linux 环境,请补充该时刻的进程 RSS:

ps -o pid,rss,vsz -p <pid>

如果您更倾向于在发生 OOM 的瞬间自动抓取,可以添加以下参数,但需要重启 JVM:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/

我们理解该转储文件会比较大,并且其中确实以字符串形式包含文档文本和学生数据。因此如果无法共享,您可以在本地使用 Eclipse MAT 打开,然后将 dominator tree(支配树,按 Retained Heap 排序的前 20 行)以及对占用最大对象执行 Path to GC Roots → exclude weak/soft references 的结果发给我们。截图即可。

另外,能否提供你们并发生成文档的相关信息 —— 工作线程数量或线程池大小,以及文档是并行生成还是串行生成?

files.zip (399.0 KB)

根据上面的过程,保留了hprof文件和截图,hprof文件提示上传失败,没有超过200M。先将其他文件提供出来,供参考。因多次测试,pid可能不一样,运行结果都是同样的情况。Aspose.word 26.5版本。
相同的程序、相同的参数,20.2版本运行就没有内存溢出问题。期间也更新了几个版本,比如24.5、25.3、26.3,均有内存问题。目前生产环境一直是退回到20.2版本运行,但是20.2版本又有一些缺陷是无法解决的。
工作线程只有一个,文件一个一个生成,附件压缩包里有示例文件,看看是否可排查出什么问题?

@dhzhao2016 感谢您提供的堆转储文件。您的堆中包含 101,409 个 sun.font.TrueTypeFont 对象,但它们只指向 8 个不同的字体文件:

36,641  timesi.ttf          13,803  round_brackets1854.ttf
25,215  times.ttf            1,594  horizontal.ttf
22,031  math1.ttf              797  brack_sm.ttf
                               796  /tmp/simsun.ttcSimSun.tmp
                               532  timesbi.ttf

同样的几个字体文件被反复打开了数万次,且从未释放。每个对象都持有一块本地内存映射(DirectByteBuffer 共 101,416 个,与字体一一对应),这正是您的 RSS 达到 11.2 GB、而 Java 堆仅为 2 GB 的原因。内存消耗在堆外的本地内存中 —— 这也解释了为什么 cleanup()clearCaches()deepClone() 都没有效果,以及为什么我们之前基于堆内存的测试什么都没有发现。

这些是 JDK 的字体类(sun.font.*),并非 Aspose.Words 的文档对象。我们使用您提供的示例试卷,搭建了 Linux + Java 8 的容器环境:

JVM 结果
OpenJDK 8u181 每份文档增加 4.9 个字体对象,线性增长,从不释放
Temurin 8u492 稳定在 44,保持不变

相同的 Aspose.Words 版本、相同的文档、相同的代码 —— 唯一的差异是 JVM。这强烈表明根本原因是 JDK 8 字体管理器的缺陷,而非 Aspose.Words。在我们的测试中,Aspose.Words 20.1 同样出现了泄漏(每份文档 +3.9)—— 与 26.5 相差无几。因此,我们目前还无法解释为什么 20.2 在您那里是正常的。

在我们测试的所有版本中,保存为 PDF 从未加载过任何一个 JDK 字体对象。而您的转储中却大量存在这些对象,这说明您的流程中一定还有渲染图片的环节,而不仅仅是生成 PDF。

在更换 Aspose.Words 版本之前,请先尝试将 JDK 升级到最新的 Java 8 版本(或 Java 11/17),同时保持当前的 Aspose.Words 版本不变。在我们的测试中,仅此一项就消除了增长。相比继续停留在 20.2,这个方案的影响要小得多。

补充问题:

  1. 您的确切 JVM 版本 —— java -version 的完整输出。另外,使用的是 Oracle JDK 还是 OpenJDK?您的转储中包含 sun.font.T2KFontScaler,该类仅存在于 Oracle JDK 8 中。
  2. 当 20.2 正常运行时,JDK 是否也是同一个版本?如果升级过程中 JVM、基础镜像或操作系统也发生了变化,那么真正的变量可能并不是 Aspose.Words 的版本。
  3. 您的流程中是否有渲染图片的环节 —— 页面预览、缩略图、save(PNG/JPEG),或者对已渲染的页面调用 getPageCount()/updatePageLayout()?仅保存 PDF 不会产生这些字体对象,因此中间还缺少一个环节。
  4. math1.ttfround_brackets1854.ttfbrack_sm.ttfhorizontal.ttf 来自哪里?它们看起来像是 WIRIS/MathType 的公式字体,并被安装到了系统级目录。是手动添加到 /usr/share/fonts/ 的吗?
  5. 能否在非生产环境中进行一次 JDK 升级测试,并反馈在约 600 份文档时 RSS 是否仍会达到 10 GB?

十分感谢您的回复,通过您提供的思路,我们经过大量测试,原有使用的是Oracle JDK 171(或221),均出现了泄漏问题,此问题是在JDK 301版本之后修复的,所以至少要更新到Oracle JDK 301 解决字体泄漏问题。
同时,我们经过评估,将生产环境升级至Java 17, Temurin jdk-17.0.20+8,全面解决字体泄漏问题,包括WIRIS/MathType 的公式字体,并同时更新Aspose.word版本至26.7最新版本。经大量测试,没有再出现内存泄漏问题。
再次感谢您的技术分析和指导!

1 Like

@dhzhao2016 很高兴得知问题已解决。如果您有任何其他疑问或遇到任何问题,请随时与我们联系。我们非常乐意为您提供帮助。