今天学习了一些关于文件上传操作中可能造成的java虚拟机大对象存储问题,菜鸟第一次发帖总结,失误、错误漏洞百出,希望帖子没有达到不忍直视的地步……
首先,看段文件上传的代码:
public Upload(ServletConfig config,HttpServletRequest request){
m_application = config.getServletContext();
m_request = request;
m_totalBytes = m_request.getContentLength();
m_binArray = new byte[m_totalBytes + 2];
for(;totalRead<m_totalBytes;totalRead+=readBytes){
try{
m_request.getInputStream();
readBytes = m_request.getInputStream().read(m_binArray,totalRead,m_totalBytes - totalRead);
}
}
}
由以上两段代码可以看出来上传文件是从HttpServletRequest的getInputStream()中陆续读取出来,
读取之前会直接根据Request中内容的长度申请一个byte数组,此处为大的内存申请的根本原因,而且从后续的代码可以看出来,Request中允许包含多个对象,多个对象一起上传的话就会全部装载在同一个byte数组中,等全部上传完毕后,文件的内容全都临时存放在这块内存中(即byte数组中),接着在完成了一段业务逻辑分析之后,再依次从数组中把文件内容字节读取出来,调用saveAs方法写入到本地硬盘中,该流程的相关代码段如下:
for(int i =0;i<m_files.getCount();i++){
if(!m_files,getFile(i).isMissing()){
m_files,getFile(i).saveAs(destPathName + m_files.getFile(i),option);
count++;
}
}
public void savaAs(String (destPathName,int optionSaveAs) throws IOException{
System.out.println("IOException occur!");
try{
java.io.File file = new java.io.Flie(path);
FileOutputStream fileOut = new FileOutputStream(file);
fileOut.write(m_parent.m_binArray,m_startData,m_size);
fileOut.close();
}
}
这样做有一个很大的缺陷,就是在并发量逐渐增加的时候,会占用大量的内存空间来存放这些文件内容字节,而且这些字节都是要占用JVM堆内存的连续空间。此外由于在I/O读写时需要占据一定的时间,造成了即使这段时间内客户端上传完毕了也还要保持与服务端始终连接,等待服务端处理完业务逻辑的分析并写入硬盘空间之后才能断开连接,同时JVM中的文件占用内存才能得到回收的问题。
解决问题的办法是,采用缓冲式I/O读写,绝不使用JVM堆内存来保存整个文件
,示例代码如下:
InputStream in = request.getInputStream();
byte[] buf = new byte[1024*64];
File file = new java.io.File(path);
FileOutputStream fileOut = new FileOutputStream(file);
int len = 0;
while((len = in.read(buf))>0){
fileOut.write(buf,0,len);
}
这种原则是每次上传时,申请一个适中大小的byte数组做缓冲数组(参考64kB大小),直接从request的InputStream中直接依次读取64KB数据到此byte数组中,然后马上写入本地文件(JVM堆内存只需要维护这个64KB数组),这样就杜绝了JVM中直接申请对象超过64KB的可能。另一好处就是,该办法还能防止在GC日志中频繁出现因为内存碎片造成的明明有大量堆空间,却分配失败,迫使进行GC的现象发生。