处理含Base64编码PDF的.dat文件时堆内存溢出问题求解
问题描述
我会接收一个.dat文件,其中包含多个以Base64字符串编码的PDF文件,各编码内容以换行符或特定字符分隔。初始处理流程为:读取文件→用splitBy "\n"拆分载荷→遍历每个拆分项→解码Base64→保存为PDF文件。该方案处理小体积.dat文件时正常,但处理大文件时触发Java堆内存溢出错误,推测原因是splitBy操作将整个文件内容加载到内存中。请问该问题如何修复?是否有更优的解决方案?
当前Mule流配置代码
<flow name="dat-to-pdfFlow" doc:id="7f23d7a6-7187-454b-bd60-8e0319b52028" > <file:listener doc:name="Read .DAT" doc:id="aba64085-5b24-48b6-a6d6-f10658c991f1" config-ref="File_Config" directory="/Users/test/Work/POC/input" autoDelete="true" recursive="false" outputMimeType="application/octet-stream; streaming=true"> <scheduling-strategy > <fixed-frequency /> </scheduling-strategy> </file:listener> <logger level="INFO" doc:name="Logger" doc:id="ebd52647-c467-459f-bdeb-a30a997aba76" message="Read .DAT from #[attributes.path]"/> <ee:transform doc:name="Transform Message" doc:id="dbc765a9-ba1f-49be-b996-a7a883c8a6c5"> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 output application/java --- payload splitBy "\n"]]></ee:set-payload> </ee:message> </ee:transform> <parallel-foreach doc:name="Parallel For Each" doc:id="7ba70e6b-630a-4259-9ad6-fb4b5c197402"> <vm:publish doc:name="Publish" doc:id="7d1c9b34-6150-4195-b142-45ef43a9e2db" config-ref="VM_Config" queueName="write" /> </parallel-foreach> <logger level="INFO" doc:name="Logger" doc:id="72bc4cfc-383d-4c95-add9-409bd4fdfeeb" message="Completed" /> </flow> <flow name="consume-pdf" doc:id="9e531c3b-b4b6-4348-9848-bef76df138bb" > <vm:listener doc:name="Listener" doc:id="5907406d-d4cf-4d5b-a82a-8a3829fcc425" config-ref="VM_Config" queueName="write" outputMimeType="text/plain"/> <ee:transform doc:name="Transform Message" doc:id="aacf98b0-b212-46bf-b615-abadfddc87f7"> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 import * from dw::core::Binaries output multipart/form-data --- { parts:{ base64Content:{ headers:{ "Content-Type":"application/pdf" }, content: fromBase64(payload) }, } } ]]></ee:set-payload> </ee:message> </ee:transform> <set-payload value="#[payload]" doc:name="Set Payload" doc:id="215630ca-ebd1-4eb7-9325-536f034eaff3" mimeType="application/pdf" /> <file:write doc:name="Write" doc:id="87623dd2-6051-4918-ab36-f76bf1c9544e" config-ref="File_Config" path="#[('/Users/test/Work/POC/output/' ++ uuid() ++ '.pdf')]" mode="APPEND" /> </flow>
报错日志
java.lang.OutOfMemoryError: Java heap space Dumping heap to /Applications/AnypointStudio.app/Contents/Eclipse/plugins/org.mule.tooling.server.4.9.ee_7.21.0.202502030106/mule/logs/dump_mule-393ef4bd-6139-49d5-bc8a-3401c8045277.hprof ... JVM received a signal SIGKILL (9). Heap dump file created [419386973 bytes in 0.255 secs] # # java.lang.OutOfMemoryError: Java heap space # -XX:OnOutOfMemoryError="""/Applications/AnypointStudio.app/Contents/Eclipse/plugins/org.mule.tooling.server.4.9.ee_7.21.0.202502030106/mule/bin/kill.sh""" %p # Executing """/Applications/AnypointStudio.app/Contents/Eclipse/plugins/org.mule.tooling.server.4.9.ee_7.21.0.202502030106/mule/bin/kill.sh""" 66579"... JVM process is gone. JVM exited unexpectedly. Automatic JVM Restarts disabled. Shutting down. <-- Wrapper Stopped
问题根源
你的推测完全正确:splitBy "\n"操作会将整个流式载荷一次性加载到内存中,生成完整的字符串列表,大文件场景下直接耗尽堆内存。虽然文件监听器开启了streaming=true,但后续的splitBy强制将流转换为内存中的集合,彻底破坏了流式处理的优势。
修复与优化方案
核心思路是保持流式处理,逐行读取并处理,避免一次性加载整个文件到内存,具体调整如下:
1. 用流式拆分替代内存式拆分
移除原有Transform Message中的splitBy,改用DataWeave的splitAsStream函数实现流式拆分,该函数会返回一个流式迭代器,按需读取内容而非一次性加载全部:
%dw 2.0 import * from dw::core::Streams output application/java streaming=true --- payload splitAsStream by "\n"
2. 控制并发数,避免内存过载
并行处理会同时加载多个Base64字符串到内存,进一步加剧内存压力。大文件场景下建议使用普通For Each,并通过maxConcurrency参数控制并发数(比如设为2-4,根据服务器内存配置调整),避免同时处理过多任务。
3. 简化Base64解码与写入流程
原有消费流中输出multipart/form-data属于多余操作,直接解码为二进制流后写入文件即可,减少不必要的内存开销:
%dw 2.0 import * from dw::core::Binaries output application/octet-stream --- fromBase64(payload)
同时可移除多余的set-payload组件,直接用file:write写入文件。
4. 临时缓解:调整JVM堆内存
如果流式优化后仍有内存压力,可临时增大JVM堆内存。在Mule的wrapper.conf中修改:
wrapper.java.maxmemory=4096
注意这只是临时方案,核心优化仍需依赖流式处理。
优化后的完整Mule流示例
主处理流
<flow name="dat-to-pdfFlow" doc:id="7f23d7a6-7187-454b-bd60-8e0319b52028" > <file:listener doc:name="Read .DAT" doc:id="aba64085-5b24-48b6-a6d6-f10658c991f1" config-ref="File_Config" directory="/Users/test/Work/POC/input" autoDelete="true" recursive="false" outputMimeType="application/octet-stream; streaming=true"> <scheduling-strategy > <fixed-frequency /> </scheduling-strategy> </file:listener> <logger level="INFO" doc:name="Logger" doc:id="ebd52647-c467-459f-bdeb-a30a997aba76" message="Read .DAT from #[attributes.path]"/> <ee:transform doc:name="Transform Message" doc:id="dbc765a9-ba1f-49be-b996-a7a883c8a6c5"> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 import * from dw::core::Streams output application/java streaming=true --- payload splitAsStream by "\n" ]]></ee:set-payload> </ee:message> </ee:transform> <foreach doc:name="For Each" doc:id="7ba70e6b-630a-4259-9ad6-fb4b5c197402" maxConcurrency="2"> <vm:publish doc:name="Publish" doc:id="7d1c9b34-6150-4195-b142-45ef43a9e2db" config-ref="VM_Config" queueName="write" /> </foreach> <logger level="INFO" doc:name="Logger" doc:id="72bc4cfc-383d-4c95-add9-409bd4fdfeeb" message="Completed" /> </flow>
消费流
<flow name="consume-pdf" doc:id="9e531c3b-b4b6-4348-9848-bef76df138bb" > <vm:listener doc:name="Listener" doc:id="5907406d-d4cf-4d5b-a82a-8a3829fcc425" config-ref="VM_Config" queueName="write" outputMimeType="text/plain"/> <ee:transform doc:name="Transform Message" doc:id="aacf98b0-b212-46bf-b615-abadfddc87f7"> <ee:message> <ee:set-payload><![CDATA[%dw 2.0 import * from dw::core::Binaries output application/octet-stream --- fromBase64(payload) ]]></ee:set-payload> </ee:message> </ee:transform> <file:write doc:name="Write" doc:id="87623dd2-6051-4918-ab36-f76bf1c9544e" config-ref="File_Config" path="#[('/Users/test/Work/POC/output/' ++ uuid() ++ '.pdf')]" mode="OVERWRITE" /> </flow>
关键优化总结
- 使用
splitAsStream替代splitBy,全程保持流式处理,避免一次性加载整个文件 - 控制并发数,防止并行处理导致的内存激增
- 简化数据转换流程,减少不必要的中间格式转换
- 优先通过流式架构优化解决内存问题,而非单纯增大堆内存
内容的提问来源于stack exchange,提问作者veejay

