Spring Boot文件上传场景下直接内存未释放的问题及优化方案咨询
我太能理解你遇到这个问题的焦虑了——直接内存持续上涨不释放,要是碰到大文件或者高并发上传,分分钟就会触发OutOfMemoryError,搞崩服务稳定性。咱们先拆解下当前代码的核心问题,再一步步给你梳理靠谱的优化方案。
为什么你的直接内存会越用越高?
你当前用file.getBytes()把整个文件加载到byte[]里,这个操作在Spring处理Multipart请求的底层,其实可能依赖了DirectByteBuffer来做临时存储。而堆外内存(直接内存)的回收逻辑是:只有当对应的DirectByteBuffer对象被GC回收后,它关联的Cleaner机制才会被动释放堆外内存。但GC的触发时机是不确定的,尤其是如果你的堆内存还没到阈值,GC可能迟迟不执行,导致直接内存被一直占用。再加上你把整个文件全量加载到内存,大文件场景下这个问题会被无限放大。
核心优化方案:流式处理,从根源避免全量加载
这是解决问题最关键的一步——不要把整个文件读到内存里,而是用流式IO边读边写,完全规避大内存占用的问题。
1. 改造代码:用InputStream流式存储文件
把原来依赖byte[]的逻辑改成基于InputStream的流式处理,这样不管文件多大,内存里只会保留一小段缓冲数据,不会占满直接内存或堆内存。
改造后的MyEntityService核心代码示例:
@Transactional public ResponseEntity<String> addFileVersion(MultipartHttpServletRequest request) throws IOException { try { log.info("Before reading : {} bytes", getDirectMemoryUsed()); MultipartFile file = request.getFile("file"); if (file == null) { return ResponseEntity.badRequest().body("The file has not been transferred or the file is empty."); } String fileName = file.getOriginalFilename(); // 用InputStream流式处理,替代file.getBytes() try (InputStream inputStream = file.getInputStream()) { storeFile(inputStream, fileName); } log.info("After reading : {} bytes", getDirectMemoryUsed()); return ResponseEntity.ok("ok"); } catch (IOException e) { log.error("Error processing file", e); return ResponseEntity.badRequest().body("Error when uploading file: " + e.getMessage()); } finally { log.info("Finally reading : {} bytes", getDirectMemoryUsed()); } } private void storeFile(InputStream inputStream, String path) throws IOException { String decode = UriUtils.decode(path, StandardCharsets.UTF_8); Path savedPath = Paths.get("/storage/tmp", decode); // 确保父目录存在 Files.createDirectories(savedPath.getParent()); // 流式写入文件,避免全量加载到内存 Files.copy(inputStream, savedPath, StandardCopyOption.REPLACE_EXISTING); }
2. 优化Spring Multipart配置,减少内存占用
在application.properties(或yml)里配置Multipart的参数,让大文件自动落到磁盘临时文件,而不是占用内存:
# 单个文件最大大小 spring.servlet.multipart.max-file-size=100MB # 单个请求最大大小 spring.servlet.multipart.max-request-size=200MB # 超过这个阈值的文件会写到磁盘临时文件,而不是内存(比如设置1MB) spring.servlet.multipart.file-size-threshold=1MB # 临时文件存储路径(可选,默认是系统临时目录) spring.servlet.multipart.location=/tmp/spring-multipart
这个配置会让Spring把超过阈值的Multipart文件先写到磁盘,而不是在内存里处理,直接减少直接内存的占用压力。
关于直接内存回收的补充说明
虽然不推荐主动干预GC,但如果你确实遇到极端场景需要促发DirectByteBuffer回收,可以考虑:
- 不要主动调用
System.gc()(这只是给JVM一个GC提示,不能保证立即执行),更推荐通过优化代码(比如流式处理)从根源减少内存占用,让JVM自动在合适时机触发GC。 - 可以通过JMX监控
java.nio.BufferPool.direct的指标(比如MemoryUsed、Capacity),或者用Arthas的vm命令实时查看直接内存使用情况,确认优化效果。
额外优化点:解决synchronized锁的性能瓶颈
你原来的storeFile里用了synchronized(this),在高并发上传场景下,这个锁会导致所有请求串行处理,严重影响性能。建议改成按文件路径分段锁,比如:
private final ConcurrentHashMap<String, Object> locks = new ConcurrentHashMap<>(); private void storeFile(InputStream inputStream, String path) throws IOException { String decode = UriUtils.decode(path, StandardCharsets.UTF_8); Path savedPath = Paths.get("/storage/tmp", decode); Files.createDirectories(savedPath.getParent()); // 按文件路径获取锁,只有同一个文件的上传会串行,不同文件不互斥 Object lock = locks.computeIfAbsent(savedPath.toString(), k -> new Object()); synchronized (lock) { Files.copy(inputStream, savedPath, StandardCopyOption.REPLACE_EXISTING); } // 可选:如果文件不会被重复上传,处理完可以移除锁,避免内存泄漏 locks.remove(savedPath.toString()); }
总结
最核心的优化思路是流式处理+磁盘落盘,从根源上避免把整个文件加载到内存里。这样不管是堆内存还是直接内存的占用都会大幅降低,GC也能更高效地回收不必要的对象,直接内存上涨的问题自然就解决了。
内容来源于stack exchange

