记一次 PDF 转 Word 服务频繁崩溃的排查与解决:Java 堆内存并不是全部 摘要一个基于 Spring Boot Aspose.PDF 的 PDF 转 Word 在线工具部署在 2C2G 云服务器上频繁出现服务进程被系统杀死的问题。初步怀疑是 Java 堆内存不足但调大 -Xmx 后反而崩溃更快。经过逐步排查最终定位到根因Aspose.PDF 底层使用 JNI 调用 C 引擎其内存消耗在 Java 堆之外的原生内存Native Memory区域而这块区域的大小不受 -Xmx 限制。本人在线小工具www.onltool.site 欢迎大家进来体验批评和指正有兴趣的小伙伴进来看看。问题背景技术栈Spring Boot 2.5 MyBatis-PlusAspose.PDF 23.1system scopejar 包71MBApache POI 5.2.5Word、Excel、PPT 处理服务器配置2 核 2G 内存症状上传一个不到 2MB 的 PDF 文件进行转换服务端 Java 进程直接被操作系统杀死没有任何 Java OOM 异常日志OutOfMemoryError也没有业务异常重启后再次转换依旧崩溃原启动参数nohupjava-Xms512m-Xmx1024m-jarocean-system-1.0.0.jar/dev/null21排查过程碰过的壁尝试 1怀疑 Aspose 内部优化方法开销太大当前 pdfToWord()方法中Aspose 加载 PDF 后调用了 pdfDocument.optimizeResources()。查文档发现这个方法做的是合并重复资源、移除未使用对象、优化字体和图片——全部针对 PDF 内部结构对输出 Word 文档毫无影响。 操作移除 optimizeResources()调用。 结果无效仍然崩溃。尝试 2怀疑转换后内存未及时释放Aspose.PDF 底层是 C 引擎通过 JNI 调用。Document.close()只释放 Java 层的包装对象C 层的原生内存需要等待 GC 触发 finalizer 才回收。于是主动加了 System.gc()来提前回收。 操作在 Document 的 try-with-resources 块结束后调用 System.gc()。 结果崩溃更频繁了。事后分析System.gc()在全量 GC 时会触发 Aspose C 对象的 finalizer如果 finalizer 有并发 bug 或原生内存状态不一致直接导致 JVM 进程 SIGSEGV。这是一个危险的优化。尝试 3分片转换 并行处理把大 PDF 按页拆成5页一组的小 PDF每片单独转 DOCX用4线程并行处理最后合并成一个 Word 文档。理论上每片的内存占用只有全量加载的1/N。 操作 - PdfFileInfo 轻量获取页数 - PdfFileEditor.extract(start, end)流式拆分 - ExecutorService.newFixedThreadPool(4)并行转 - mergeDocx()用 ZIP 级 XML拼接合并 结果转换后的 Word 文档质量严重下降——跨页表格被切断、样式在各片之间不一致、页眉页脚丢失。分片破坏了 PDF 文档的结构完整性无法接受。 操作回退分片逻辑。尝试 4反复调整 JVM 堆大小逻辑推断服务器总共 2G-Xmx 占多了留给操作系统的就少了堆小了 Java 层面又不够。-Xmx结果 1024m 崩溃 768m 崩溃 512m 崩溃 反复调了几轮全都崩溃。到这一步开始怀疑问题根本不在 Java 堆里。根因分析运行dmesg|tail-10查看系统日志 Out of memory: Killed process18473(java)total-vm:1856432kB, anon-rss:823456kB, file-rss:0kB, shmem-rss:0kBOS 的 OOM Killer 直接杀了 Java 进程。计算一下内存分布 2G 服务器总内存 ├── 操作系统预留 ~300M ├── JVM 堆(-Xmx1024m)1024M ├── Metaspace类元数据 ~200M ├── JIT CodeCache ~128M ├── 线程栈约50线程 ~100M ├── 直接内存 / MappedByteBuffer ~50M ├────────────────────────────────── │ 以上合计 ~1800M ├────────────────────────────────── │ 剩余给 Aspose native ~200M ← 远远不够 └──────────────────────────────────根本原因Aspose.PDF 底层是 C 引擎通过 JNI 调用。它需要的内存是原生内存Native Memory / Off-Heap不在 -Xmx 管控范围内。PDF 文件在磁盘上只有 2MB但 Aspose C 引擎加载时需要解码嵌入字体TrueType/OpenType渲染矢量图形展开图片像素构建页面对象树这些操作全在原生物理内存中完成。一个看似很小的 PDF内部可能嵌入了完整的中文字体包10MB或者包含高分辨率图片在内存中展开后轻松占用 300-500MB 原生内存。调大 -Xmx 为什么会更糟Java 堆越大留给原生内存的剩余空间就越小。-Xmx1024m 时原生内存只剩 ~200MAspose 稍微多用一点就爆。-Xmx512m 时原生内存有 ~700M理论上会好一些但 2G 总量实在太紧稍有波动还是扛不住。最终方案既然换不了服务器就从几个方向同时防守限制文件大小20MBapplication.ymlspring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB file-size-threshold: 3MB# 超过 3MB 溢写到磁盘不占堆内存server: tomcat: max-http-form-post-size: 20MB后端校验if(file.getSize()20*1024*1024){returnApiResult.error(文件大小不能超过 20MB);}前端预拦截handleFile(f){if(f.size20*1024*1024){this.$message.error(最大20M)return}}并发控制同一时刻只允许一个 PDF 转换private final Semaphore pdfConvertSemaphorenew Semaphore(1);PostMapping(/pdf-to-word)public ApiResultStringpdfToWord(RequestParam(file)MultipartFilefile){boolean acquiredfalse;try{acquiredpdfConvertSemaphore.tryAcquire(3, TimeUnit.SECONDS);if(!acquired){returnApiResult.error(当前转换任务繁忙请稍后重试);}returnconvert(file, f -fileConvertService.pdfToWord(f),converted.docx);}finally{if(acquired)pdfConvertSemaphore.release();}}JVM 参数调优nohupjava\-Xms128m\# 起步 128M不预占太多-Xmx768m\# 最大 768M给原生内存留空间-XX:MaxMetaspaceSize256m\# 限制类元数据上限-XX:ReservedCodeCacheSize128m\# 限制 JIT 编译缓存-XX:UseG1GC\# G1 回收器大堆停顿低-Dfile.encodingUTF-8\-jarocean-system-1.0.0.jar/dev/null212G 内存分配最终版 ├── 操作系统 ~300M ├── JVM 堆(-Xmx768m)768M ├── Metaspace上限 256M ├── CodeCache上限 128M ├── 线程栈 ~100M ├───────────────────────────── │ 以上合计 ~1550M ├───────────────────────────── │ 剩余给 Aspose native ~450M ← 够小 PDF 使用 └─────────────────────────────总结误区正确认知内存不够就调大-Xmx-Xmx只管 Java 堆Aspose 这类 JNI 框架吃的是堆外原生内存调大-Xmx反而挤占原生内存空间小 PDF 文件不会耗多少内存PDF 内部可能嵌入完整字体包、高分辨率图片内存展开后可达文件大小的50-100 倍System.gc()能解决内存问题在 JNI 场景下System.gc()可能触发原生层 finalizer 的并发 bug导致进程直接崩溃分片转换降低内存分片破坏了 PDF 文档结构完整性跨页元素丢失质量不可接受Java 进程被杀会有 OOM 日志原生内存耗尽时OS OOM Killer 直接SIGKILL进程Java 根本没机会写日志核心经验排查 Java 服务崩溃先看 dmesg。如果看到 Out of memory: Killed process说明是 OS 层面的 OOM不是 JVM 层面的 OutOfMemoryError。这种情况调 -Xmx 没用要检查 JNI 原生内存消耗或者直接升服务器配置。2G 服务器跑 Aspose.PDF 就是走钢丝没有银弹。