使用ServletOutputStream在Java servlet中编写非常大的文件,而不存在内存问题

我正在使用IBM Websphere Application Server v6和Java 1.4,并试图将大型CSV文件写入ServletOutputStream以供用户下载。 目前文件从50-750MB不等。

较小的文件不会造成太多的问题,但是对于较大的文件,它看起来正在写入堆中,然后导致出现OutOfMemory错误并closures整个服务器。

这些文件只能通过HTTPS传递给已通过身份validation的用户,这就是为什么我通过Servlet服务他们,而不是将他们粘在Apache中。

我使用的代码是(在这个附近删除了一些绒毛):

  resp.setHeader("Content-length", "" + fileLength); resp.setContentType("application/vnd.ms-excel"); resp.setHeader("Content-Disposition","attachment; filename=\"export.csv\""); FileInputStream inputStream = null; try { inputStream = new FileInputStream(path); byte[] buffer = new byte[1024]; int bytesRead = 0; do { bytesRead = inputStream.read(buffer, offset, buffer.length); resp.getOutputStream().write(buffer, 0, bytesRead); } while (bytesRead == buffer.length); resp.getOutputStream().flush(); } finally { if(inputStream != null) inputStream.close(); } 

FileInputStream似乎不会导致问题,如果我写入另一个文件或只是完全删除写入内存使用情况似乎没有出现问题。

我在想, resp.getOutputStream().write被存储在内存中,直到数据可以被发送到客户端。 所以整个文件可能被读取并存储在resp.getOutputStream()导致我的内存问题和崩溃!

我已经尝试缓冲这些stream,并尝试使用来自java.nio通道,其中没有一个似乎对我的内存问题有任何区别。 我也刷新OutputStream每循环迭代一次,循环后,这并没有帮助。

平均体面servletcontainer本身刷新stream默认每大约2KB。 您应该确实没有必要按顺序在从同一个源相继stream式传输数据的时间间隔上显式调用HttpServletResponseOutputStream上的flush() 。 在例如Tomcat(和Websphere!)中,这可以被configuration为HTTP连接器的bufferSize属性。

如果内容长度事先是未知的(根据Servlet API规范 !),并且客户端是否支持HTTP 1.1,那么平均体面的servletcontainer也只是将数据以块的forms进行stream式传输。

问题症状至less表明servletcontainer在刷新之前正在缓冲内存中的整个stream。 这可能意味着没有设置内容长度标题和/或servlet容器不支持分块编码和/或客户端不支持分块编码(即使用HTTP 1.0)。

要解决这个问题,只需预先设置内容长度:

 response.setHeader("Content-Length", String.valueOf(new File(path).length())); 

flush工作在输出stream上。

真的,我想评论一下,你应该使用三种forms的写入,因为缓冲区不一定是完全读取的(特别是在文件末尾(!))。 另外一个尝试/最后会是为了,除非你想让你的服务器意外死亡。

我已经使用了一个包装输出stream的类来使其在其他上下文中可重用。 它能够更快地获取数据到浏览器,但是我没有看到内存的影响。 (请原谅我过时的m_variables命名)

 import java.io.IOException; import java.io.OutputStream; public class AutoFlushOutputStream extends OutputStream { protected long m_count = 0; protected long m_limit = 4096; protected OutputStream m_out; public AutoFlushOutputStream(OutputStream out) { m_out = out; } public AutoFlushOutputStream(OutputStream out, long limit) { m_out = out; m_limit = limit; } public void write(int b) throws IOException { if (m_out != null) { m_out.write(b); m_count++; if (m_limit > 0 && m_count >= m_limit) { m_out.flush(); m_count = 0; } } } } 

我也不确定ServletOutputStream上的flush()是否适用于这种情况,但ServletResponse.flushBuffer()应该将响应发送给客户端(至less每2.3 servlet规范)。

ServletResponse.setBufferSize()听起来也很有希望。

所以,按照你的情况,你不应该在while循环(每次迭代)中刷新(而不是在外面)吗? 我会尝试,尽pipe有一个更大的缓冲区。

  1. 如果在close()运算符中不是null,那么Kevin的类应该closuresm_out字段,我们不想泄漏这些东西,对吗?

  2. ServletOutputStream.flush()运算符一样, HttpServletResponse.flushBuffer()操作也可以刷新缓冲区。 但是,它似乎是一个实现的具体细节,这些操作是否有任何作用,或http内容长度支持是否有干扰。 请记住,指定内容长度是HTTP 1.0的一个选项,所以事情应该只是stream出来,如果你刷新的东西。 但是我没有看到

while条件不起作用,在使用之前需要检查-1。 并且请为输出stream使用一个临时variables,它更好地阅读,并保险箱可重复地调用getOutputStream()。

 OutputStream outStream = resp.getOutputStream(); while(true) { int bytesRead = inputStream.read(buffer); if (bytesRead < 0) break; outStream.write(buffer, 0, bytesRead); } inputStream.close(); out.close(); 

与你的记忆问题无关,while循环应该是:

 while(bytesRead > 0); 

你的代码有一个无限循环。

 do { bytesRead = inputStream.read(buffer, offset, buffer.length); resp.getOutputStream().write(buffer, 0, bytesRead); } while (bytesRead == buffer.length); 

偏移在整个循环中都有相同的值,所以如果初始偏移量= 0 ,它将在每次迭代中保持如此,这将导致无限循环,这将导致OOM错误。

ibm websphere应用程序服务器默认使用servlet的asynchronous数据传输。 这意味着它缓冲响应。 如果您在处理大数据和OutOfMemoryexception时遇到问题,请尝试更改WAS上的设置以使用同步模式。

将WebSphere Application Server WebContainer设置为同步模式

您还必须注意加载块并冲洗它们。 从大文件加载样本。

 ServletOutputStream os = response.getOutputStream(); FileInputStream fis = new FileInputStream(file); try { int buffSize = 1024; byte[] buffer = new byte[buffSize]; int len; while ((len = fis.read(buffer)) != -1) { os.write(buffer, 0, len); os.flush(); response.flushBuffer(); } } finally { os.close(); }