【问题标题】:java.lang.OutOfMemoryError: Java heap space during bytea downloadjava.lang.OutOfMemoryError:bytea 下载期间的 Java 堆空间
【发布时间】:2016-05-07 17:43:27
【问题描述】:

我正在使用此代码从 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();
    }
}

但是当我开始下载代码时,我得到 java.lang.OutOfMemoryError: Java heap space on this line:

 buf = new byte[binaryStreasm.available()];

我能以某种方式优化代码以减少内存消耗吗?

更新代码:

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();
        }
    }

【问题讨论】:

  • 请注意,available() 不是流的实际大小。它只是可以在不阻塞的情况下读取的字节数的估计 - 它可以小于流的大小,甚至可以更大。 documentation 明确表示使用此方法的返回值来分配旨在保存此流中所有数据的缓冲区是不正确的
  • 好的,我该如何解决这个问题?
  • 我还看到 ResultSet 对象 rs 没有关闭。这也可能是内存泄漏。
  • 好的,我更新了代码。
  • 如果抛出异常,您的结果集仍然不会关闭。假设您使用的是 Java 7 或更新版本,您应该使用 try-with-resources 块来确保 ResultSet 在最后关闭。在旧版本中,使用 try/finally。

标签: java postgresql postgresql-9.3


【解决方案1】:

您在这里对InputStream::available 的依赖是错误的。它的documentation 说:

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

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

(强调我的)

所以你面临两个问题:

  1. 如果您没有输出流的确切大小,应在 Content-Length 标头中传递什么数字?
  2. 如何在不将所有数据同时保存在内存中的情况下将流从结果集传递到响应。

我将通过稍微更改查询来解决第一个问题,使其返回bytea 字段的实际大小。

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

PostgreSQL 函数 octet_length 为您提供 bytea 列的长度(以字节为单位)。

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

ec.setResponseContentLength(rs.getInt("file_length"));

现在,对于第二个问题,您应该避免将所有内容都读入一个大缓冲区。如果你使用rs.getInt("file_length') 中的数字来分配缓冲区,你会遇到同样的内存问题。您应该逐渐复制流。

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

IOUtils.copy( rs.getBinaryStream("FILE"), response.getOutputStream() );

如果您不想使用 Apache Commons IO,您可以编写自己的循环 - 使用 small 缓冲区。同样,首先设置响应内容类型和长度,然后执行类似

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 );
    }

}

然后完成响应,关闭结果集,就完成了。我使用了 try-with-resources 语法,它在完成后会自动关闭流。

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

【讨论】:

  • 我更新了帖子。现在,当我尝试下载大小为 518 Mb 的文件时,我得到 javax.faces.el.EvaluationException: org.postgresql.util.PSQLException: ERROR: invalid memory alloc request size 1088381651
  • 好的,帖子已更新。这个问题有解决办法吗?
  • @PeterPenzov 是数据库中的所有文件还是大文件?
  • 我可以成功下载2-3兆左右的PDF文件。大约 500 兆字节的文件会出现此问题。
  • @PeterPenzov 这似乎是 PostgreSQL 服务器端内存分配的问题。您或许应该尽量不要使用bytea,而是使用 BLOBS。
【解决方案2】:

这可能是因为binaryStreasm.available() 返回一个大值,以至于它试图创建一个无法放入内存的字节数组,尝试设置像5121024 这样的小值。

这里的另一个问题是您尝试将bytea 对象的全部内容加载到内存中,这不是正确的方法,特别是如果您必须处理像这里这样的大二进制内容。您应该将内容写入response.getOutputStream()或先将其写入临时文件,然后将文件内容写入response.getOutputStream()

【讨论】:

  • 现在我得到Caused by: org.postgresql.util.PSQLException: ERROR: invalid memory alloc request size 1088381651
  • 它从何而来?什么是堆栈跟踪?
  • 当我开始下载文件时出现。
  • 你能告诉我如何将二进制对象写入response.getOutputStream()的代码示例吗?
  • @PeterPenzov 你已经在 RealSkeptic 的回答中得到了它
猜你喜欢
  • 2021-09-09
  • 1970-01-01
  • 1970-01-01
  • 2011-10-27
  • 2022-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-12
相关资源
最近更新 更多