1. 问题现象与背景分析
上周在维护一个企业级文档管理系统时,遇到了一个典型的内存溢出(OOM)问题。当用户尝试上传超过500MB的PDF文件时,服务端Java进程会突然崩溃,日志中出现"java.lang.OutOfMemoryError: Java heap space"错误。这个系统原本设计支持最大2GB的文件上传,理论上不应该出现这种情况。
经过排查发现,问题出在文件上传的临时存储处理环节。系统采用的是传统的Spring MVC文件上传方式,在文件被写入磁盘前,整个文件内容会被完整加载到内存中。当多个用户同时上传大文件时,堆内存很快就被耗尽。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内存溢出原理深度解析
2.1 JVM内存模型与OOM机制
Java堆内存是JVM中对象实例的主要存储区域,其大小通过-Xmx参数配置。当应用程序试图分配超过堆剩余空间的对象时,就会抛出OOM错误。在我们的案例中,每个上传请求都会在内存中创建完整的文件字节数组,导致:
- 一个500MB文件上传 = 至少500MB堆内存占用
- 默认堆大小通常为1GB(-Xmx1g)
- 两个并发上传就会耗尽内存
2.2 传统文件上传的内存陷阱
Spring的MultipartFile接口默认实现(如StandardMultipartFile)会将上传文件全部缓存在内存或临时文件中。关键问题在于:
- 文件内容被完整读取到byte[]数组
- 即使配置了临时文件存储,大文件仍可能先被缓存在内存
- 内存占用峰值=上传文件大小×并发数
3. 解决方案设计与实现
3.1 方案选型对比
我们评估了三种主流解决方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 增加堆内存 | 调大-Xmx参数 | 改动最小 | 治标不治本,无法应对持续增长 |
| 分块上传 | 客户端分片上传 | 内存占用恒定 | 需要前端改造 |
| 流式处理 | 直接写入磁盘 | 服务端独立解决 | 需要重构上传逻辑 |
最终选择流式处理方案,因其可以:
- 保持现有API接口不变
- 内存占用与文件大小无关
- 兼容各种客户端
3.2 流式上传实现代码
java复制@PostMapping("/upload")
