为什么上传文件会导致内存溢出?
内存溢出的核心原因是 试图将整个文件一次性加载到内存中,让我们看看一个典型的、有问题的上传代码流程:

- 客户端发送一个文件(一个 500MB 的视频文件)到服务器。
- 服务器端的代码(一个 Servlet)接收到这个 HTTP 请求。
- 为了处理文件,代码中可能会有类似这样的操作:
// 错误示范:将整个请求体读取到一个字节数组中 byte[] fileBytes = request.getInputStream().readAllBytes(); // 或者使用 Apache Commons IO // byte[] fileBytes = IOUtils.toByteArray(request.getInputStream());
readAllBytes()方法会一直读取输入流,直到数据结束,然后将所有数据都存储在内存中的一个byte[]数组里。- 如果上传的文件是 500MB,JVM 堆中就需要立即分配 500MB 的连续空间来存储这个数组,如果服务器内存不足,或者同时有多个大文件上传,JVM 堆内存很快就会被耗尽,从而抛出
OutOfMemoryError。
问题出在“全量加载”这个行为上。 无论是 byte[]、String 还是 InputStream 直接转成内存对象,只要文件大小超过了 JVM 剩余的可用内存,就会溢出。
解决方案:如何避免内存溢出?
解决这个问题的关键思想是:流式处理,不一次性加载整个文件,我们应该像水管一样,让数据从客户端流到服务器,再从服务器流到最终的存储位置(如硬盘、云存储),而不是用一个巨大的水桶(内存)去接住所有水。
以下是几种主流的解决方案,从推荐到备选排序:
使用 Servlet 3.0+ 的 Part API (推荐,原生且简单)
如果你的应用运行在支持 Servlet 3.0 的服务器(如 Tomcat 7+, Jetty 9+, Spring Boot 内嵌服务器等)上,这是最简单、最直接的方法。

工作原理:
HttpServletRequest 提供了一个 getPart() 方法,它返回一个 Part 对象。Part 对象本质上是一个封装了上传文件数据的 InputStream,你可以直接从这个 InputStream 中读取数据,并写入到文件系统,而无需将整个文件内容加载到内存。
示例代码:
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
// 在 Servlet 类上添加 @MultipartConfig 注解
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB 的内存缓冲区,超过这个大小就写入临时文件
maxFileSize = 1024 * 1024 * 100, // 单个文件最大 100MB
maxRequestSize = 1024 * 1024 * 200 // 整个请求最大 200MB
)
public class FileUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 1. 获取上传的文件部分
Part filePart = request.getPart("file"); // "file" 是前端 input 的 name 属性
String fileName = getFileName(filePart); // 从 Part 对象中获取原始文件名
// 2. 定义目标存储路径
String uploadPath = getServletContext().getRealPath("") + File.separator + "uploads";
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
String filePath = uploadPath + File.separator + fileName;
// 3. 核心:使用 NIO 的 Files.copy 进行流式复制
// 这是最关键的一步,它会将 InputStream 的数据直接写入到文件系统,
// 而不会在内存中保存整个文件。
try (InputStream fileContent = filePart.getInputStream()) {
Files.copy(fileContent, Paths.get(filePath), StandardCopyOption.REPLACE_EXISTING);
}
response.getWriter().println("File " + fileName + " uploaded successfully to " + filePath);
}
private String getFileName(final Part part) {
final String partHeader = part.getHeader("content-disposition");
for (String content : partHeader.split(";")) {
if (content.trim().startsWith("filename")) {
return content.substring(content.indexOf('=') + 1).trim().replace("\"", "");
}
}
return null;
}
}
前端 HTML:
<form action="your-upload-servlet-url" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
使用成熟的框架库 (如 Apache Commons FileUpload)
如果你使用的是较旧的 Servlet 版本(2.x),或者需要更强大的功能(如进度监听、更灵活的配置),Apache Commons FileUpload 是一个绝佳的选择。

工作原理:
它通过 DiskFileItemFactory 来管理内存和临时文件,当上传的文件大小超过一个阈值时,它会自动将文件内容从内存转移到服务器的临时磁盘上,从而保护内存。
Maven 依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
示例代码:
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
public class CommonsFileUploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String UPLOAD_DIRECTORY = "uploads";
private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3; // 3MB
private static final int MAX_FILE_SIZE = 1024 * 1024 * 40; // 40MB
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 50; // 50MB
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 检查是否是 multipart 请求
if (!ServletFileUpload.isMultipartContent(request)) {
// 如果不是,则停止
PrintWriter writer = response.getWriter();
writer.println("Error: Form must has enctype=multipart/form-data.");
writer.flush();
return;
}
// 配置上传参数
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(MEMORY_THRESHOLD);
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(MAX_FILE_SIZE);
upload.setSizeMax(MAX_REQUEST_SIZE);
// 构建上传路径
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIRECTORY;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
try {
// 解析请求内容,获取文件项
@SuppressWarnings("unchecked")
List<FileItem> formItems = upload.parseRequest(request);
if (formItems != null && formItems.size() > 0) {
for (FileItem item : formItems) {
// 处理不在表单中的字段(即文件)
if (!item.isFormField() && item.getName() != null) {
String fileName = new File(item.getName()).getName();
String filePath = uploadPath + File.separator + fileName;
File storeFile = new File(filePath);
// 在此处写入文件,item.write() 内部也是流式操作
item.write(storeFile);
System.out.println("File " + fileName + " uploaded successfully!");
}
}
}
} catch (Exception ex) {
throw new ServletException("Error uploading file", ex);
}
response.getWriter().println("File uploaded successfully!");
}
}
直接操作 InputStream (基础方案)
如果你不想依赖任何库,也可以自己处理 HttpServletRequest 的 InputStream,这种方法更底层,需要自己处理一些边界情况,但原理是一样的。
示例代码:
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
public class ManualInputStreamServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 注意:这种方法无法直接获取文件名,需要从请求头解析
String fileName = "uploaded_file_" + System.currentTimeMillis() + ".dat";
String uploadPath = getServletContext().getRealPath("") + File.separator + "uploads";
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdir();
}
String filePath = uploadPath + File.separator + fileName;
// 使用 try-with-resources 确保 Stream 被正确关闭
try (InputStream is = request.getInputStream();
OutputStream os = Files.newOutputStream(Paths.get(filePath))) {
byte[] buffer = new byte[4096]; // 4KB 的缓冲区
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
response.getWriter().println("File " + fileName + " uploaded successfully.");
}
}
最佳实践和注意事项
-
配置合理的内存阈值:
- 对于
@MultipartConfig或DiskFileItemFactory,设置一个合理的fileSizeThreshold(如 1MB 或 10MB),这意味着小于这个大小的文件会保存在内存中,大于这个大小的文件会写入临时磁盘,这样可以平衡性能和内存使用。
- 对于
-
限制上传文件大小:
- 必须在服务器和代码层面都设置文件大小限制,防止恶意用户上传超大文件耗尽服务器资源。
- 服务器层面:在 Tomcat 的
conf/web.xml中配置maxPostSize。 - 代码层面:使用
@MultipartConfig的maxFileSize或ServletFileUpload的setFileSizeMax。
-
清理临时文件:
- 使用
Commons FileUpload时,上传成功后,如果文件被写入临时目录,最好手动删除FileItem对象(通过delete()方法),避免磁盘空间被占满。 - 对于
@MultipartConfig,服务器通常会在请求结束后自动清理临时文件,但了解其行为总是好的。
- 使用
-
考虑云存储:
- 对于生产环境,特别是大文件上传,最佳实践是先将文件上传到你的应用服务器,然后立即将文件异步地转移到专业的云存储服务(如 Amazon S3, Google Cloud Storage, 阿里云 OSS)。
- 这样做的好处:
- 可靠性:云存储提供了持久性和高可用性。
- 可扩展性:云存储可以轻松应对海量文件存储需求。
- 性能:云存储服务通常能提供更好的下载速度和 CDN 加速。
- 安全性:可以利用云存储的访问控制策略。
-
异步处理:
- 如果上传文件后还需要进行耗时处理(如视频转码、图片压缩),不要在请求线程中同步处理,这会导致客户端长时间等待,应该将任务放入消息队列(如 RabbitMQ, Kafka)或使用异步任务框架(如 Spring
@Async),让请求线程快速返回一个“上传成功”的响应,后台再慢慢处理。
- 如果上传文件后还需要进行耗时处理(如视频转码、图片压缩),不要在请求线程中同步处理,这会导致客户端长时间等待,应该将任务放入消息队列(如 RabbitMQ, Kafka)或使用异步任务框架(如 Spring
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Servlet 3.0+ Part API |
简单、原生、无额外依赖、代码简洁 | 需要 Servlet 3.0+ 环境 | 强烈推荐,适用于所有现代 Java Web 应用。 |
| Apache Commons FileUpload | 功能强大、灵活、兼容旧版 Servlet | 需要额外引入库、代码稍显繁琐 | 需要兼容旧项目或需要高级功能(如进度条)时。 |
直接操作 InputStream |
无依赖、完全控制 | 代码复杂、需手动处理细节(如文件名)、容易出错 | 学习或特殊需求,不推荐用于生产项目。 |
在任何新的 Java Web 项目中,优先使用 Servlet 3.0+ 的 Part API,它完美地解决了内存溢出问题,并且使用起来非常简单,核心原则永远是 “流式处理,避免全量加载”。
