You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot文件上传场景下直接内存未释放的问题及优化方案咨询

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回收,可以考虑:

  1. 不要主动调用System.gc()(这只是给JVM一个GC提示,不能保证立即执行),更推荐通过优化代码(比如流式处理)从根源减少内存占用,让JVM自动在合适时机触发GC。
  2. 可以通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 07:04:43