InputStreamResource占满堆内存致K8容器被杀死的内存泄漏问题咨询
问题根因分析
现有代码的疏漏主要有3点:
- 全量加载内存:直接调用
decoder.decode(INPUT_STREAM_AS_STRING)会将整个编码字符串一次性解码为完整的字节数组,整个文件内容全部加载到堆内存中。如果文件体积较大、并发请求较多,会生成大量大字节数组,这类大对象会直接进入JVM老年代,常规Young GC无法回收,只有触发Full GC才会清理,而堆内存600MB的阈值下,很容易在Full GC触发前就占满堆空间。 - 双倍内存占用:编码字符串本身在堆中占用一份空间,解码后的字节数组又占用一份同等大小的空间,同一份数据产生两倍内存开销,进一步加剧堆内存消耗。
- 无流式处理逻辑:所有内容先全部加载到内存再返回给客户端,内存占用和文件大小完全成正比,大文件场景下堆内存必然被打满。
无内存泄漏的最佳实现方案
核心思路是流式解码+流式响应,全程不生成全量字节数组,仅通过固定大小的缓冲区做数据流转,内存占用恒定在KB级,不会随文件大小增长。
Spring 提供的StreamingResponseBody支持异步写响应流,配合流式解码器即可实现该需求,示例代码如下:
import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Base64; // 类内业务方法 public ResponseEntity<StreamingResponseBody> downloadItem(Item item) { final String encodedContent = INPUT_STREAM_AS_STRING; final Base64.Decoder decoder = Base64.getDecoder(); StreamingResponseBody responseBody = outputStream -> { // try-with-resources自动关闭流,避免资源泄漏 try ( // 将编码字符串转为字节流,base64为ASCII编码,用ISO_8859_1不会有编码损失 ByteArrayInputStream encodedIn = new ByteArrayInputStream(encodedContent.getBytes(StandardCharsets.ISO_8859_1)); // 包装为解码流,实现边读边解码 InputStream decodedIn = decoder.wrap(encodedIn) ) { // 8KB缓冲区拷贝,内存占用固定 byte[] buffer = new byte[8192]; int readLen; while ((readLen = decodedIn.read(buffer)) != -1) { outputStream.write(buffer, 0, readLen); // 按需flush,避免响应缓存占用过多内存 outputStream.flush(); } } }; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, ATTACHMENT_FILENAME + item.getName()) .body(responseBody); }
优化效果说明
- 内存占用恒定:不管文件大小是几MB还是几GB,运行过程中只会占用8KB的缓冲区内存,不会产生大字节数组对象,完全避免老年代内存占满的问题。
- 资源自动释放:try-with-resources语法会自动关闭所有打开的流,不会产生资源泄漏。
- 性能更高:无需等待全量解码完成即可开始返回内容,大文件场景下响应速度更快,也能避免长时间等待导致的504超时。
如果使用的不是Java内置的Base64解码器,只要解码器支持流式wrap输入流,都可以沿用该逻辑实现。
内容的提问来源于stack exchange,提问作者user1398291
相关产品推荐
相关产品推荐

