朋友最近发现,他的服务器在处理文件上传的相关请求时非常容易发生宕机问题,尤其是在进行多文件批量上传、超大文件(几百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的超大文件测试以及正式上线运行后的日志反馈,不再引发宕机问题。
在这里,我们可以把一个超大文件看作一个水池,需要将其中的水传输到其他地方。第一种写法就是直接将水池中所有的水存放在服务器上,再通过服务器传输到其他地方,而服务器根本无法一次性负载这么多的水,于是服务器就被「淹死」了。而第二种写法,就是通过服务器在大水池和目的地之间架设一条大小适宜的水管,通过水管将水池中的水不断地传输到目的地。这样服务器完全能够承受任何「一刻」所负载的水量,自然就不会出现被「撑死」的问题。
在我们生活中也有许多类似的道理,饭不是直接端着锅就往嘴巴里倒,而是用碗盛起一小部分,然后一口一口地吃。饭吃得太急会噎死,水喝得太急会呛死。在服务器上这个道理也同样适用。无论是Java、C#还是其他编程语言,文件流都能起到如上类似的作用。而其他类似的大数据处理问题,也可以采用这样的思路来解决。