发送HTTP时出现OutputStream内存溢出错误

15

我正在尝试将本地文件系统中的大视频/图像文件发布到HTTP路径,但在一段时间后遇到了内存不足错误...

这是代码:

public boolean publishFile(URI publishTo, String localPath) throws Exception {
    InputStream istream = null;
    OutputStream ostream = null;
    boolean isPublishSuccess = false;

    URL url = makeURL(publishTo.getHost(), this.port, publishTo.getPath());
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();


    if (conn != null) {

        try {

            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setRequestMethod("PUT");
            istream = new FileInputStream(localPath);
            ostream = conn.getOutputStream();

            int n;
            byte[] buf = new byte[4096];
            while ((n = istream.read(buf, 0, buf.length)) > 0) {
                ostream.write(buf, 0, n); //<--- ERROR happens on this line.......???
            }

            int rc = conn.getResponseCode();

            if (rc == 201) {
                isPublishSuccess = true;
            }

        } catch (Exception ex) {
            log.error(ex);
        } finally {
            if (ostream != null) {
                ostream.close();
            }

            if (istream != null) {
                istream.close();
            }
        }
    }

    return isPublishSuccess;

}

这是我遇到的错误信息...

Exception in thread "Thread-8773" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2786)
    at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:94)
    at sun.net.www.http.PosterOutputStream.write(PosterOutputStream.java:61)
    at com.test.HTTPClient.publishFile(HTTPClient.java:110)
    at com.test.HttpFileTransport.put(HttpFileTransport.java:97)

有些人(包括我在内)认为交叉张贴很粗鲁:http://forums.sun.com/thread.jspa?threadID=5424210,特别是当你甚至没有提到这个事实时。 - Joachim Sauer
请不要对我批评您的代码的修改感到冒犯。它比平均水平要好,但还有改进的空间。所有非平凡的代码都是如此。而且异常处理很容易出错:在一周左右的时间里,当我只是输入一个尝试/捕获/最终示例而没有让编译器检查时,我得到了应得的-1分。 - kdgregory
6个回答

16
HttpUrlConnection会缓冲数据,以便设置Content-Length标头(根据HTTP规范)。如果您的目标服务器支持,则可以使用“分块传输”作为另一种选择,这样每次只缓冲一小部分数据。不过,并非所有服务都支持它(例如 Amazon S3 就不支持)。
另一种选择(我认为更好的选择)是使用Jakarta HttpClient。您可以从文件中在请求中设置“entity”,连接代码将适当地设置请求标头。
编辑:nos评论说 OP 可以调用 HttpURLConnection.setFixedLengthStreamingMode(long length) 方法。我不知道这个方法;自从那时以来我就没有再使用过这个类。
然而,我仍然建议使用 Jakarta HttpClient,因为它减少了 OP 需要维护的代码量。代码虽然很常见,但仍有潜在错误的风险:
  • OP 正确处理了输入输出之间的复制循环。通常,当我看到这个示例时,发布者要么没有正确检查返回的缓冲区大小,要么不断重新分配缓冲区。恭喜你,但是现在您必须确保您的后继者也同样小心。
  • 异常处理不是很好。是的,操作者记得在finally块中关闭连接,恭喜他做到了这一点。但是,任何一个close()调用都可能抛出IOException异常,导致另一个无法执行。而且该方法作为一个整体抛出Exception异常,所以编译器不会帮助捕获类似的错误。
  • 我数了一下设置和执行响应的代码有31行(不包括响应代码检查和URL计算,但包括try/catch/finally)。使用HttpClient,代码只需要大约半打行。
  • 即使操作者已经完美地编写了此代码,并将其重构为类似于Jakarta Commons IO中的方法,他/她也不应该这样做。这段代码已经被其他人编写和测试过了。我知道重新编写它是浪费我的时间,而且我怀疑这对操作者来说也是浪费时间。


    8
    在这种情况下,根据文件大小,也可以调用HttpURLConnection.setFixedLengthStreamingMode方法。这将导致HttpUrlConnection返回一个直接流,因为它不需要缓冲以确定内容长度。 - nos
    nos 是正确的。完全没有必要使用分块请求或使用第三方库来实现此功能。只需在 HttpURLConnection 上调用 setFixedLengthStreamingMode 并传入文件长度,然后连接并获取 getOutputStream 将返回一个非缓冲的直接包装器以访问套接字的 OutputStream。 - jarnbjo
    1
    @jarnbjo - 我猜你是那个给我和其他回答者投反对票的人。虽然你的评论很有价值,但把它们作为回复发表会更有建设性。 - kdgregory
    1
    @jarnbjo - 你说得对。你没有任何有建设性的意见。无论如何,只要让你开心就好了。顺便说一句,我对你的赞成票:反对票比例印象深刻。 - kdgregory
    1
    @jarnbjo - 编辑了帖子并添加了更多的解释,希望这能让你明白为什么“一行修改”(实际上是两行)“绝对不是最简单的解决方案”。 - kdgregory
    显示剩余2条评论

    5
    conn.setFixedLengthStreamingMode((int) new File(localpath).length());
    

    为了缓冲,您可以使用BufferedOutputStreamBufferedInputStream来包装您的流。

    一个很好的分块上传示例可以在这里找到:gdata-java-client


    2
    问题在于HttpURLConnection类使用字节数组来存储您的数据。可以推测您要上传的视频占用的内存超过了可用内存。您有几个选择:
    1. 增加应用程序的内存。您可以使用-Xmx1024m选项为应用程序提供1GB的内存。这将增加您可以在内存中存储的数据量。

    2. 如果您仍然遇到内存不足的问题,您可能需要考虑尝试使用另一个库来上传视频,该库不会一次性将所有数据存储在内存中。Apache Commons HttpClient就有这样的功能。请参见此网站以获取更多信息:http://hc.apache.org/httpclient-3.x/features.html。请参见此部分以了解大文件的多部分表单上传:http://hc.apache.org/httpclient-3.x/methods/multipartpost.html


    2

    除了基本的GET操作外,内置的java.net HTTP工具并不是很好用。建议使用Apache Commons HttpClient来进行操作。它可以让你做更加直观的操作,像这样:

    PutMethod put = new PutMethod(url);
    put.setRequestEntity(new FileRequestEntity(localFile, contentType));
    int responseCode = put.executeMethod();
    

    这个工具可以替代大量样板代码。


    2

    HttpsURLConnection#setChunkedStreamingMode(1024 * 1024 * 10); //10MB分块 这可以确保任何文件(任何大小)在https连接上流式传输,而不需要内部缓冲。当文件大小或内容长度未知时应使用此功能。


    -2

    你的问题在于你试图将 X 个视频字节放入 X/N 字节的 RAM 中,其中 N > 1。

    你可以将视频读入较小的缓冲区并在进行写操作时输出,或者使文件变小,或者增加进程可用的内存。

    检查你的堆大小。如果你使用默认值,可以使用 -Xmx 来增加它。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接