2013年8月16日 星期五

HttpUrlConnection 中解決 out of memory error

當程式要實作模擬 HTTP 的 GET 或 POST 行為時,通常會使用 Apache 的 httpcomponent 套件
或者是自己利用 Java 的 HttpUrlConnection 去實作~
而當使用 HttpUrlConnection 時,遇到 POST 很大的資料時,可能就會出現 OutOfMemoryError 了。

會出現 OutOfMemoryError 的原因,其實是因為 HttpUrlConnection 的 getOutputStream() 中有以下的實作方法:
if (streaming()) {
  if (strOutputStream == null) {
    if (fixedContentLength != -1) {
      strOutputStream = new StreamingOutputStream (ps, fixedContentLength);
    } else if (chunkLength != -1) {
      strOutputStream = new StreamingOutputStream (new ChunkedOutputStream (ps, chunkLength), -1);
    }
  }
  return strOutputStream;
} else {
  if (poster == null) {
    poster = new PosterOutputStream();
  }
  return poster;
}
可以看出,如果沒有做特別的設定時,HttpUrlConnection.getOutputStream() 回應的 OutputStream 是 StreamingOutputStream 的 instance
而 StreamingOutputStream 從 OutOfMemoryError 的 stackstrace 中又可以看出是以 ByteArrayOutputStream 實作的
因此實際上,HttpUrlConnection 在送出 POST 資料時,是把要送出的資料全部 cache 在記憶體上
最後要送出時會做一個 Array.copy() 的動作,把 byte array 複製後才送出,這也就是導致 POST 很大的資料(例如上傳檔案)時會記憶體不足的原因了。

解決方法如 [1] 的說明,其實看到上面的程式碼,大概就可以發現~如果說有設定 fix length 或者是 chunk length 時,回應的 OutputStream 似乎有點不同。
根據 HttpUrlConnection 文件的描述,呼叫 HttpUrlConnection.setFixedLengthStreamingMode(int) 時,可以指定 OutputStream 的長度
但文件同時提到,似乎是不能輸入超過指定長度的資料,同時也不能在資料尚未填滿前提前把 OutputStream 關閉。
比較合適的作法似乎是使用 HttpUrlConnection.setChunkedStreamingMode(int),以下轉錄文件上對於這個 method 的描述:
This method is used to enable streaming of a HTTP request body without internal buffering, when the content length is not known in advance. In this mode, chunked transfer encoding is used to send the request body. Note, not all HTTP servers support this mode.

參考資料:
1、HttpURLConnection的流式输出的缺陷和解决方法( out of memory)
2、sun.net.www.protocol.http.HttpURLConnection

沒有留言: