springboot上传大文件时内存溢出的可能解决办法

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

发布了126 篇原创文章 · 获赞 37 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/huweijian5/article/details/85067412