JAX-RS流式传输JCR大文件时堆内存溢出问题排查
JCR大文件流式传输堆内存溢出问题解决指引
我基于Jackrabbit的Magnolia CMS开发JAX-RS接口,要流式传输JCR工作区里的分片文件——主节点为file,子节点是存储为Base64编码字符串的文件分片。测试50MB-400MB的文件没问题,但处理1.2GB左右的文件时会触发堆内存溢出,以下是具体的解决方向:
原代码问题分析
原代码的核心隐患在于:每个分片的Base64字符串会被完整加载到内存,解码成byte数组后才写入输出流。对于大文件的分片来说,大量分片的字符串和byte数组会快速耗尽堆内存。
原代码片段:
@POST @Path("/getFile/{fileId}") @Produces({MediaType.APPLICATION_OCTET_STREAM}) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 401, message = "ANONYMOUS TRIED TO ACCESS ENDPOINT") }) public Response getFile(@PathParam("fileId") String fileId) { AssetsManager assetsManager = new AssetsManager(); try { Node asset = assetsManager.getAssetById(fileId); Node assetData = asset.getNode("dataNodes"); int sliceCount = asset.getProperty("lastSlice").getDecimal().intValue(); StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException { try { for (int i = 0; i <= sliceCount; i++) { Node sliceNode = assetData.getNode(String.valueOf(i)); byte[] decodedBytes = Base64.decodeBase64(sliceNode.getProperty("data").getString()); output.write(decodedBytes); output.flush(); } } catch (RepositoryException ex) { // Handle RepositoryException appropriately } finally { output.close(); } } }; return Response.status(200).entity(stream) .header("Content-Disposition", String.format("attachment; filename=%s", asset.getProperty("name").getString())) .header("Content-type", "application/octet-stream") .header("Access-Control-Expose-Headers", "Content-Disposition") .type(MediaType.APPLICATION_OCTET_STREAM) .build(); } catch (Exception ex) { return Response.status(400).entity(ex).build(); } return Response.status(500).entity(ex).build(); }
具体解决方向
1. 流式处理Base64解码,避免全量加载
不要一次性把整个Base64分片字符串加载到内存,改用流式读取和解码:
- 用
Property.getBinary()获取分片的二进制流,替代getString() - 使用
Base64InputStream(Apache Commons Codec提供)直接对输入流做解码,配合固定大小的缓冲区(比如8KB)将数据写入输出流,全程内存占用稳定在缓冲区大小,不会随分片大小增长。
2. 及时释放JCR资源,避免内存泄漏
JCR的节点、属性、Binary对象都会占用内存,处理完每个分片后要及时释放:
- 用try-with-resources自动管理Binary、输入流的资源,确保用完即关闭
- 处理完分片后,调用
Property.dispose()释放属性资源 - 确保JCR会话在使用完毕后调用
logout()关闭,避免会话缓存积累过多节点对象
3. 优化分片大小(可选)
如果当前分片尺寸过大(比如几十MB级),可以考虑减小分片大小,进一步降低单次处理的内存占用峰值。
4. 临时调整JVM堆内存(治标)
如果当前JVM堆内存分配过小,可临时调大-Xmx参数(比如-Xmx4G),但这只是缓解手段,核心还是要从代码层面优化内存使用。
优化后代码示例
@POST @Path("/getFile/{fileId}") @Produces({MediaType.APPLICATION_OCTET_STREAM}) @ApiResponses(value = { @ApiResponse(code = 200, message = "OK"), @ApiResponse(code = 401, message = "ANONYMOUS TRIED TO ACCESS ENDPOINT") }) public Response getFile(@PathParam("fileId") String fileId) { AssetsManager assetsManager = new AssetsManager(); try { Node asset = assetsManager.getAssetById(fileId); Node assetData = asset.getNode("dataNodes"); int sliceCount = asset.getProperty("lastSlice").getDecimal().intValue(); String fileName = asset.getProperty("name").getString(); StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream output) throws IOException { Session session = null; try { session = asset.getSession(); byte[] buffer = new byte[8192]; // 8KB缓冲区,可根据实际调整 for (int i = 0; i <= sliceCount; i++) { Node sliceNode = assetData.getNode(String.valueOf(i)); Property dataProp = sliceNode.getProperty("data"); try (Binary binary = dataProp.getBinary(); InputStream base64In = binary.getStream(); InputStream decodedIn = new Base64InputStream(base64In)) { // 流式复制数据,无全量内存加载 int bytesRead; while ((bytesRead = decodedIn.read(buffer)) != -1) { output.write(buffer, 0, bytesRead); } output.flush(); } finally { if (dataProp != null) { dataProp.dispose(); } sliceNode = null; // 帮助GC回收节点引用 } } } catch (RepositoryException ex) { throw new IOException("读取文件分片失败", ex); } finally { if (session != null) { try { session.logout(); } catch (Exception ignored) {} } output.close(); } } }; return Response.status(200).entity(stream) .header("Content-Disposition", String.format("attachment; filename=%s", fileName)) .header("Content-type", "application/octet-stream") .header("Access-Control-Expose-Headers", "Content-Disposition") .type(MediaType.APPLICATION_OCTET_STREAM) .build(); } catch (Exception ex) { return Response.status(400).entity(ex.getMessage()).build(); } }
内容的提问来源于stack exchange,提问作者roivasot
相关产品推荐
相关产品推荐

