最近客户反应开发的一个采购管理系统在上传大的文件时会出现宕机卡死的问题,尤其是在进行多文件批量上传、超大文件(几百MB或上GB)上传时极其容易发生。日志信息显示,引发的异常为致命异常
java.lang.OutOfMemoryError Java heap space
。
通过查看其文件上传的相关源代码发现类似如下代码内容:
byte[] fileData = formFile.getFileData(); //获取上传文件的字节数据 if (fileData != null && fileData.length > 0) { FileOutputStream fos = new FileOutputStream(destFilePath); BufferedOutputStream bos = new BufferedOutputStream(fos, 1024); bos.write(fileData); bos.flush(); bos.close(); }
其中的变量formFile是Struts1中表示通过前台表单上传的文件类org.apache.struts.upload.FormFile的一个实例。其getFileData()方法返回的就是文件内容的字节数组。
按照朋友的代码写法,即是一次性获取上传文件中的字节数据并写入保存到指定的输出流中。当上传的文件非常大时,该字节数组也巨大无比,
Java虚拟机没有足够的内存来为该字节数组分配空间,从而引发该致命异常。
将上述部分代码进行如下改写:
InputStream inputStream = formFile.getInputStream(); FileOutputStream fos = new FileOutputStream(destFilePath); BufferedOutputStream bos = new BufferedOutputStream(fos, 1024); int length = 0; byte[] buffer = new byte[1024]; while ((length = inputStream.read(buffer)) != -1) { bos.write(buffer, 0, length); } bos.flush(); bos.close(); inputStream.close();
如上改写后,经过多次上传几GB的超大文件测试以及正式上线运行后的日志反馈,不再引发宕机问题。
在这里,我们可以把一个超大文件看作一个水池,需要将其中的水传输到其他地方。第一种写法就是直接将水池中所有的水存放在服务器上,再通过服务器传输到其他地方,而服务器根本无法一次性负载这么多的水,于是服务器就被「淹死」了。而第二种写法,就是通过服务器在大水池和目的地之间架设一条大小适宜的水管,通过水管将水池中的水不断地传输到目的地。这样服务器完全能够承受任何「一刻」所负载的水量,自然就不会出现被「撑死」的问题。