java.lang.OutOfMemoryError:bytea下载期间的java堆空间

java.lang.OutOfMemoryError:bytea下载期间的java堆空间,java,postgresql,postgresql-9.3,Java,Postgresql,Postgresql 9.3,我正在使用以下代码从PostgreSQL下载bytea对象: public void initFileDBData() throws SQLException, IOException { if (ds == null) { throw new SQLException("Can't get data source"); } Connection conn = ds.getConnection(); if (conn == null)

我正在使用以下代码从PostgreSQL下载bytea对象:

public void initFileDBData() throws SQLException, IOException
{
    if (ds == null)
    {
        throw new SQLException("Can't get data source");
    }
    Connection conn = ds.getConnection();

    if (conn == null)
    {
        throw new SQLException("Can't get database connection");
    }

    PreparedStatement ps = null;

    try
    {
        conn.setAutoCommit(false);
        ps = conn.prepareStatement("SELECT * FROM PROCEDURE_FILES WHERE ID = ?");

        ps.setInt(1, id);
        ResultSet rs = ps.executeQuery();
        while (rs.next())
        {
            String file_name = rs.getString("FILE_NAME");
            InputStream binaryStreasm = rs.getBinaryStream("FILE");
            FacesContext fc = FacesContext.getCurrentInstance();
            ExternalContext ec = fc.getExternalContext();

            ec.responseReset();
            ec.setResponseContentLength(binaryStreasm.available());
            ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + file_name + "\"");

            byte[] buf;

                buf = new byte[binaryStreasm.available()];
                int offset = 0;
                int numRead = 0;
                while ((offset < buf.length) && ((numRead = binaryStreasm.read(buf, offset, buf.length - offset)) >= 0))
                {
                    offset += numRead;
                }

            HttpServletResponse response
                = (HttpServletResponse) FacesContext.getCurrentInstance()
                .getExternalContext().getResponse();

            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment;filename=" + file_name);
            response.getOutputStream().write(buf);
            response.getOutputStream().flush();
            response.getOutputStream().close();
            FacesContext.getCurrentInstance().responseComplete();
        }

    }
    finally
    {
        if (ps != null)
        {
            ps.close();
        }
        conn.close();
    }
}
我可以优化代码以减少内存消耗吗

更新代码:

public void initFileDBData() throws SQLException, IOException
    {
        if (ds == null)
        {
            throw new SQLException("Can't get data source");
        }
        Connection conn = ds.getConnection();

        if (conn == null)
        {
            throw new SQLException("Can't get database connection");
        }

        PreparedStatement ps = null;

        try
        {
            conn.setAutoCommit(false);
            ps = conn.prepareStatement("SELECT *, octet_length(FILE) as file_length FROM PROCEDURE_FILES WHERE ID = ?");

            ps.setInt(1, id);
            ResultSet rs = ps.executeQuery();
            while (rs.next())
            {
                String file_name = rs.getString("FILE_NAME");
                FacesContext fc = FacesContext.getCurrentInstance();
                ExternalContext ec = fc.getExternalContext();

                ec.responseReset();
                ec.setResponseContentLength(rs.getInt("file_length"));
                ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + file_name + "\"");

                HttpServletResponse response
                    = (HttpServletResponse) FacesContext.getCurrentInstance()
                    .getExternalContext().getResponse();

                byte[] buffer = new byte[4096];

                try (InputStream input = rs.getBinaryStream("FILE");
                    OutputStream output = response.getOutputStream())
                {
                    int numRead = 0;

                    while ((numRead = input.read(buffer)) != -1)
                    {
                        output.write(buffer, 0, numRead);
                    }
                }

                response.setContentType("application/octet-stream");
                response.setHeader("Content-Disposition", "attachment;filename=" + file_name);
                response.getOutputStream().write(buffer);
                response.getOutputStream().flush();
                response.getOutputStream().close();
                FacesContext.getCurrentInstance().responseComplete();
            }
        }
        finally
        {
            if (ps != null)
            {
                ps.close();
            }
            conn.close();
        }
    }

这可能是因为
binaryStreasm.available()
返回一个大值,它试图创建一个无法放入内存的字节数组,尝试设置小值,如
512
1024


这里的另一个问题是,您试图将
bytea
对象的全部内容加载到内存中,这不是正确的方法,尤其是当您必须处理像这里这样的大二进制内容时。您应该将内容写入
response.getOutputStream()
,或者先将其写入临时文件,然后将文件内容写入
response.getOutputStream()
这可能是因为
binaryStreasm.available()
返回一个大值,以便它尝试创建无法放入内存的字节数组,尝试设置小值,如
512
1024


这里的另一个问题是,您试图将
bytea
对象的全部内容加载到内存中,这不是正确的方法,尤其是当您必须处理像这里这样的大二进制内容时。您应该将内容写入
response.getOutputStream()
,或者先将其写入临时文件,然后将文件内容写入
response.getOutputStream()
您对
InputStream::available
的依赖是错误的。它说:

返回可从此输入流读取(或跳过)的字节数的估计值,而无需下次调用此输入流的方法进行阻止。下一次调用可能是同一个线程或另一个线程。单个读取或跳过这么多字节不会阻塞,但可能读取或跳过更少的字节

注意,虽然InputStream的一些实现将返回流中的总字节数,但许多实现不会返回使用此方法的返回值来分配用于保存此流中所有数据的缓冲区是不正确的。

(强调矿山)

所以你面临两个问题:

  • 如果没有输出流的确切大小,则在
    内容长度
    标题中传递什么数字
  • 如何将流从结果集传递到响应,而不同时将所有数据保留在内存中
  • 我将通过稍微更改查询来解决第一个问题,以便它返回
    bytea
    字段的实际大小

    ps = conn.prepareStatement("SELECT *,octet_length(FILE) as file_length FROM PROCEDURE_FILES WHERE ID = ?");
    
    PostgreSQL函数
    octet\u length
    以字节为单位提供
    bytea
    列的长度

    一旦你有了它,你就可以使用

    ec.setResponseContentLength(rs.getInt("file_length"));
    
    现在,对于第二个问题,您应该避免将所有内容读入一个大的缓冲区。如果您使用
    rs.getInt(“文件长度”)中的数字来分配缓冲区,您将遇到相同的内存问题。您应该逐步复制流

    如果您有Apache Commons IO,则可以使用
    IOUtils.copy()
    将流从结果集复制到响应的输出流。在设置响应内容类型和长度之前,请避免获取二进制流,然后执行以下操作:

    IOUtils.copy( rs.getBinaryStream("FILE"), response.getOutputStream() );
    
    如果您不想使用ApacheCommons IO,您可以编写自己的循环—使用一个小缓冲区

    byte[] buffer = new byte[4096];
    
    try ( InputStream input = rs.getBinaryStream("FILE");
          OutputStream output = response.getOutputStream() ) {
    
        int numRead = 0;
    
        while ( ( numRead = input.read( buffer ) ) != -1 ) {
            output.write(buffer, 0, numRead );
        }
    
    }
    
    然后完成响应,关闭结果集,就完成了


    顺便说一句,没有理由使用
    while
    读取一行,如果查询返回多行,代码将无法工作。您可以使用简单的
    if(rs.next())
    并在
    其他
    中向用户抛出一些异常或显示一些错误

    您对
    InputStream::available
    的依赖是错误的。它说:

    返回可读取(或跳过)的字节数的估计值从该输入流读取,而不会被该输入流的方法的下一次调用阻塞。下一次调用可能是同一线程或另一个线程。单个读取或跳过这么多字节不会阻塞,但可能读取或跳过更少的字节

    请注意,虽然InputStream的某些实现将返回流中的总字节数,但许多实现不会返回。使用此方法的返回值来分配用于保存此流中所有数据的缓冲区是不正确的。

    (强调矿山)

    所以你面临两个问题:

  • 如果没有输出流的确切大小,则在
    内容长度
    标题中传递什么数字
  • 如何将流从结果集传递到响应,而不同时将所有数据保留在内存中
  • 我将通过稍微更改查询来解决第一个问题,以便它返回
    bytea
    字段的实际大小

    ps = conn.prepareStatement("SELECT *,octet_length(FILE) as file_length FROM PROCEDURE_FILES WHERE ID = ?");
    
    PostgreSQL函数
    octet\u length
    以字节为单位提供
    bytea
    列的长度

    一旦你有了它,你就可以使用

    ec.setResponseContentLength(rs.getInt("file_length"));
    
    现在,对于第二个问题,您应该避免将所有内容都读入一个大的缓冲区。如果您使用
    rs.getInt(“file_length”)
    中的数字来分配缓冲区,您将遇到相同的内存问题。你应该循序渐进地复制这条流

    如果您有Apache Commons IO,则可以使用
    IOUtils.copy()
    将流从结果集复制到响应的输出流。在设置响应内容类型和长度之前,请避免获取二进制流,然后执行以下操作:

    IOUtils.copy( rs.getBinaryStream("FILE"), response.getOutputStream() );
    
    如果您不想使用ApacheCommonsIO,您可以编写自己的循环—使用一个小的缓冲区。再次,首先设置t