springboot 上传文件内存溢出问题解决
问题出现原因
场景,上传文件到第三方存储服务器(OSS,S3,七牛云),图省事的情况下会使用InputStream上传。但是上传文件使用的MultipartFile对象在getBytes或者getInputStream时,内存中会new一个ByteArrayInputStream流数组(getBytes会在new数组占用的基础上再copy一份),这个流占用的内存与文件大小基本一致。楼主部署的应用没有配置过jvm各个区的使用内存,即默认配置。上传25M以下的文件是可以直接上传的,超过26M的文件直接抛出内存溢出,且cpu直接拉满,原因是java程序无可用内存,一直在执行fullGC操作,只得先重启服务,寻找解决方案。
tomcat部署的应用默认物理内存
Tomcat默认可以使用的内存为128MB,在较大型的应用项目中,这点内存是不够的,有可能导致系统无法运行。常见的问题是报Tomcat内存溢出错误,Out of Memory(系统内存不足)的异常,从而导致客户端显示500错误,一般调整Tomcat的使用内存即可解决此问题。
JVM默认物理内存(使用java命令执行的jar文件)
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC后调整堆的大小。
解决方案
更改jvm运行内存最大量
这个方式有些治标不治本,但是可以快速解决问题。本次修改了jvm堆中的最大内存,内存总有不够用的一次。
TOMCAT修改方式
Windows环境下修改“%TOMCAT_HOME%\bin\catalina.bat”文件,在文件开头增加如下设置:
set JAVA_OPTS=-Xms256m -Xmx512m
Linux环境下修改“%TOMCAT_HOME%\bin\catalina.sh”文件,在文件开头增加如下设置:
JAVA_OPTS=’-Xms256m -Xmx512m’
其中,-Xms设置初始化内存大小,-Xmx设置可以使用的最大内存。建议俩个值设置一样,可以避免GC之后jvm调整堆内存的大小(占用不必要的资源)。
jar包程序修改方式
java -Xms1024m -Xmx1024m -Xmn700m -Xss128k -jar xxx.jar
-Xms1024m 设置JVM促使内存为1024M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmx1024m ,设置JVM最大可用内存为1024M。
-Xmn700m:设置年轻代大小为700M。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 不熟悉不设置即可
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 不熟悉不设置即可
第二种方案(文件写入本地上传)
把文件先存到本地目录(写入到服务器上),再将file上传到第三方文件服务器上。(其实上传文件大小超过10KB就已经会被存在本地临时目录,但是MultipartFile文件中的对象是私有的,不可访问 )
尝试获取MultipartFile实例中到Resource对象,抛出以下错误。
MultipartFile file
file.getResource().getURL();
java.io.FileNotFoundException: MultipartFile resource [file] cannot be resolved to URL
上传文件部分代码
MultipartFile file;
// 父级目录地址
String localParentPath ="/app/webapps/file/"+bussinessType + "/";
//生成保存文件
File fileParent = new File(localParentPath);
if (!fileParent.exists()) {
fileParent.mkdirs();
}
// 临时文件目录
String tempFileName = localParentPath+newFileName;
log.info("临时文件目录:" + tempFileName);
File tempFile = new File(tempFileName);
file.transferTo(tempFile);
log.info("临时文件创建完成");
// 上传文件所需参数
String fileContentType = file.getContentType();
Long fileSize = file.getSize();
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(fileContentType);
objectMetadata.setContentLength(fileSize);
String fileKey = "/prod/test/20220213/name.app";
OSSConfig.OSS_CLIENT.putObject(OSSConfig.OSS_PUBLIC_BUCKET_NAME, fileKey, tempFile, objectMetadata);
另有更好的方法可以发到评论区一起讨论。
希望这篇文章可以帮到你。