问题出现原因

场景,上传文件到第三方存储服务器(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包程序修改方式

参考文档地址

JVM调优总结 -Xms -Xmx -Xmn -Xss

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);

另有更好的方法可以发到评论区一起讨论。
希望这篇文章可以帮到你。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐