springboot上传大文件时内存溢出的可能解决办法
- 在springboot中上传大文件时要考虑内存的情况,一般我们会通过在执行服务时加入-Xms512m -Xmx512m等参数加大堆内存,但这是指标不治本的,关键还是看代码处理的时候有无导致内存泄漏的原因。
例如:java -Xms512m -Xmx512m -jar rent_web-1.0.0.jar
- 有时候我们会需要把上传的文件再调用其他服务进行上传,即a把文件上传给b,b再把文件上传给c,那么在中间的这个转发服务如果处理不好就会导致内存溢出。
- 下面是导致内存溢出的代码,这里溢出的原因是RequestBody的创建用了file.getBytes(),如果你继续跟进RequestBody.create就会发现最终会调用System.arraycopy来拷贝file.getBytes(),这相当于拷贝了一份,如果本来是512M,一拷贝就变成了1024M,内存肯定吃不消,于是就抛出内存溢出。
public ApiResult<UploadResult> storeFile(MultipartFile file) throws IOException {
...
String originalName= URLEncoder.encode(file.getOriginalFilename(),"utf8");
RequestBody rb = RequestBody.create(MediaType.parse("multipart/form-data"), file.getBytes());
MultipartBody.Part part = MultipartBody.Part.createFormData("file",originalName, rb);
EFSService efsService = getRandEFSService();
Response<ApiResult<UploadResult>> res = efsService.upload(part, module).execute();
...
}
- 经过自己实践,我的思路是先把文件存到本地临时目录(其实上传文件大小超过10KB就已经会被存在本地临时目录,但由于是私有对象不可访问,当然你高兴可以用反射获得,但既然官方不开放这样的接口,那还是不要使用反射去获取了,避免以后官方实现发现了变化,那先前的反射也就会报错了),再将file置空让gc能够回收原来MultipartFile指向的内存,这样子没有拷贝多一份数据,大文件就不会多加一倍内存而溢出了。
public ApiResult<UploadResult> storeFile( MultipartFile file) throws IOException {
...
File tempFile = File.createTempFile("video", null);
Log.info("临时文件目录:" + tempFile.getCanonicalPath());
file.transferTo(tempFile);
String originalName= URLEncoder.encode(file.getOriginalFilename(),"utf8");
file=null;//file置为null是为了告诉gc此块内存可以回收
RequestBody rb = RequestBody.create(MediaType.parse("multipart/form-data"), tempFile);
MultipartBody.Part part = MultipartBody.Part.createFormData("file",originalName, rb);
EFSService efsService = getRandEFSService();
Response<ApiResult<UploadResult>> res = efsService.upload(part, module).execute();
...
}
- 本人测试过程中发现上传文件有时会出现以下这个错误,重启后会消失,奇怪的是出现这个问题时下断点发现竟然都没执行到,不知在哪里就抛出了这个异常(原因暂时不详,可能是springboot上传文件用到了临时目录,而这个临时目录又很容易被系统删除)。
Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [C:\Users\HuWeiJian\AppData\Local\Temp\tomcat.4476717864971067098.8101\work\Tomcat\localhost\ROOT] is not valid
解决办法是通过
/**
* 自定义临时文件目录,确保目录不会被系统删除导致抛异常
* @return
*/
@Bean
public MultipartConfigElement multipartConfigElement(){
MultipartConfigFactory factory=new MultipartConfigFactory();
String location =System.getProperty("user.dir")+tempFileDir;
Log.info("location="+location);
File tmpDir=new File(location);
if(!tmpDir.exists()){
tmpDir.mkdirs();
}
factory.setLocation(location);
return factory.createMultipartConfig();
}
- 本人能力有限,如有错误请不吝指教。
后话
- MultipartFile其实只是一个接口,真正实现在org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.StandardMultipartFile,
- StandardMultipartFile里面有个javax.servlet.http.Part对象,
- Part也是一个接口,真正实现在org.apache.catalina.core.ApplicationPart,
- ApplicationPart里有个org.apache.tomcat.util.http.fileupload.FileItem接口和File,
- .FileItem实现是org.apache.tomcat.util.http.fileupload.disk.DiskFileItem,File表示上传的文件(是一个临时存放文件)
- DiskFileItem是由org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory创建出来的,
- 从DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD = 10240可以得知如果文件大小在10KB以内,则直接放在内存,如果大于10KB,则存储在本地磁盘中(注意是存在临时目录中,意味着这个文件如果你不持久化那这个文件随时会被系统清除)
参考
Java中OutOfMemoryError(内存溢出)的三种情况及解决办法 - chen_lay的博客 - CSDN博客
https://blog.csdn.net/chen_lay/article/details/52209748
SpringBoot项目优化和Jvm调优(楼主亲测,真实有效) - 熊本同学 - CSDN博客
https://blog.csdn.net/wd2014610/article/details/82182617