Java壓縮流GZIPStream導致的內存泄露


轉自   https://www.jianshu.com/p/5841df465eb9

 

 

 

我們來聊聊GZIPOutputStreamGZIPInputStream, 如果不關閉流會引起的問題,以及GZIPStream申請和釋放堆外內存的流程, Let's do it!

引子

在我的工程里面又一個工具類 ZipHelper 用來壓縮和解壓 String

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * 用來壓縮和解壓字符串 */ public class ZipHelper { // 壓縮 public static String compress(String str) throws Exception { if (str == null || str.length() == 0) { return str; } ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(out); gzip.write(str.getBytes()); gzip.close(); return out.toString("ISO-8859-1"); } // 解壓縮 public static String uncompress(String str) throws Exception { if (str == null || str.length() == 0) { return str; } ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes("ISO-8859-1")); GZIPInputStream gunzip = new GZIPInputStream(in); byte[] buffer = new byte[1024]; int n; while ((n = gunzip.read(buffer)) >= 0) { out.write(buffer, 0, n); } return out.toString(); } } 

最近服務出現了占用swap空間的問題,初步定位為內存泄漏,最后通過分析定位到是 Native 方法Java_java_util_zip_Inflater_init一直在申請內存(關於分析方法可以查閱這篇博客內存泄露分析實戰)但是沒有釋放,很有可能就是流沒有關閉造成的,而這部分代碼最大的問題就是沒有在finally里面去關閉流,於是乎我打算改造這部分代碼,利用 try-with-resource 語法糖,然后代碼就被修改成了這樣:

import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * Created by jacob. * * 用來壓縮和解壓字符串 */ public class ZipHelper { /** * 壓縮字符串 * * @param str 待壓縮的字符串 * @return 壓縮后的字符串 * @throws Exception 壓縮過程中的異常 */ public static String compress(String str) throws Exception { if (str == null || str.length() == 0) { return str; } // ByteArrayOutputStream 和 ByteArrayInputStream 是一個虛擬的流, // JDk源碼中關閉方法是空的, 所以無需關閉, 為了代碼整潔,還是放到了try-with-resource里面 try (ByteArrayOutputStream out = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(out)) { gzip.write(str.getBytes()); // gzip.finish(); return out.toString("ISO-8859-1"); } } /** * 解壓字符串 * * @param str 待解壓的字符串 * @return 解壓后的字符串 * @throws Exception 解壓過程中的異常 */ public static String uncompress(String str) throws Exception { if (str == null || str.length() == 0) { return str; } try (ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayInputStream in = new ByteArrayInputStream(str.getBytes("ISO-8859-1")); GZIPInputStream gunzip = new GZIPInputStream(in)) { byte[] buffer = new byte[1024]; int n; while ((n = gunzip.read(buffer)) >= 0) { out.write(buffer, 0, n); } return out.toString(); } } } 

是不是順眼多了吶,可是這樣的代碼可以壓縮的,在解壓的時候會報錯。一開始我以為是解壓的代碼出現了問題,最后才發現是因為壓縮的時候沒有成功壓縮,導致解壓的時候無法解壓。報以下錯誤

Exception in thread "main" java.io.EOFException: Unexpected end of ZLIB input stream at java.util.zip.InflaterInputStream.fill(InflaterInputStream.java:240) at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158) at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117) at java.io.FilterInputStream.read(FilterInputStream.java:107) at coderbean.ZipHelper.uncompress(ZipHelper.java:52) at coderbean.Main.main(Main.java:12) 

好好的代碼怎么會突然壓縮失敗,后來發現的問題是在GZIPOutputStream中,在close()方法中會主動調用finish()方法。

/** * Writes remaining compressed data to the output stream and closes the * underlying stream. * @exception IOException if an I/O error has occurred */ public void close() throws IOException { if (!closed) { finish(); if (usesDefaultDeflater) def.end(); out.close(); closed = true; } } 

在下面的方法中才會將壓縮后的數據輸出到輸入流,由於原來的代碼會調用 close()方法,從而間接調用了 finish() 方法。那我我們的try-with-resource到底出了什么問題,其實問題就在於執行close()的時間。

/** * Finishes writing compressed data to the output stream without closing * the underlying stream. Use this method when applying multiple filters * in succession to the same output stream. * 在該方法中才會將壓縮后的數據輸出到輸入流,由於原來的代碼會調用 close()方法,從而 * 間接調用了 finish() 方法。 * @exception IOException if an I/O error has occurred */ public void finish() throws IOException { if (!def.finished()) { def.finish(); while (!def.finished()) { int len = def.deflate(buf, 0, buf.length); if (def.finished() && len <= buf.length - TRAILER_SIZE) { // last deflater buffer. Fit trailer at the end writeTrailer(buf, len); len = len + TRAILER_SIZE; out.write(buf, 0, len); return; } if (len > 0) out.write(buf, 0, len); } // if we can't fit the trailer at the end of the last // deflater buffer, we write it separately byte[] trailer = new byte[TRAILER_SIZE]; writeTrailer(trailer, 0); out.write(trailer); } } 

try-with-resource 執行時機和條件

try-with-resource 是在 JDK7 中新增加的語法糖(其實就是抄的C#),用來自動執行流的關閉操作,只要該類實現了AutoCloseableclose()方法。


package java.lang; public interface AutoCloseable { /** * @throws Exception if this resource cannot be closed */ void close() throws Exception; } 

實現了這個接口之后,我們可以將會在try代碼塊執行結束之后自動關閉流

try(/* 在此處初始化資源 */){ // do something } //在代碼塊執行結束前最后一步關閉流 

由於在GZIPOutputStream執行了finish()方法或者close()方法之后才會真正的將壓縮后的數據寫入流,在上文我改造的代碼中並沒有首先執行finish()方法,而是直接在try代碼塊執行完之后關閉了流 GZIPOutputStream, 由於close()方法執行在out.toString("ISO-8859-1")之后,因此壓縮並沒有真正的被執行,然而對於ZipHelper.compress()方法並沒有感知,而是返回了沒有壓縮成功的字符串,從而造成在解壓的時候報錯。

為什么會引起的堆外內存泄漏

通過最開始的代碼我們可以看出,在沒有發生異常的情況下,compress()方法是可以正常的關閉流的,所以內存泄露的根源應該是在uncompress()方法,通過跟蹤GZIPInputStream的構造函數和close()應該很快就能找到答案。

下面是申請堆外內存和釋放堆外內存的過程調用圖,可以對比代碼參考


 
堆外內存調用釋放流程圖

由於篇幅的原因就不將JDK源碼注釋一同貼上來了,感興趣的同學可以按圖索驥,找到對應的注釋。

//java.util.zip.GZIPInputStream.java public class GZIPInputStream extends InflaterInputStream { public GZIPInputStream(InputStream in) throws IOException { this(in, 512); //調用下面的構造函數 } public GZIPInputStream(InputStream in, int size) throws IOException { super(in, new Inflater(true), size); //新建 Inflater 對象 usesDefaultInflater = true; readHeader(in); } public void close() throws IOException { if (!closed) { super.close(); //這里的父類是java.util.zip.InflaterInputStream eos = true; closed = true; } } } 
//java.util.zip.Inflater.java public class Inflater { public Inflater(boolean nowrap) { zsRef = new ZStreamRef(init(nowrap)); } /** * Closes the decompressor and discards any unprocessed input. * This method should be called when the decompressor is no longer * being used, but will also be called automatically by the finalize() * method. Once this method is called, the behavior of the Inflater * object is undefined. */ public void end() { synchronized (zsRef) { long addr = zsRef.address(); zsRef.clear(); if (addr != 0) { end(addr); buf = null; } } } // 此處調用了 Native 方法 private native static long init(boolean nowrap); private native static void end(long addr); } 
//java.util.zip.InflaterInputStream.java public class InflaterInputStream extends FilterInputStream { /** * Closes this input stream and releases any system resources associated * with the stream. * @exception IOException if an I/O error has occurred */ public void close() throws IOException { if (!closed) { if (usesDefaultInflater) inf.end(); in.close(); closed = true; } } } 

openJDK 中 JVM 關於這個本地方法的實現

JNIEXPORT jlong JNICALL
Java_java_util_zip_Inflater_init(JNIEnv *env, jclass cls, jboolean nowrap) { //此處使用 calloc 申請了堆外內存 z_stream *strm = calloc(1, sizeof(z_stream)); if (strm == NULL) { JNU_ThrowOutOfMemoryError(env, 0); return jlong_zero; } else { const char *msg; int ret = inflateInit2(strm, nowrap ? -MAX_WBITS : MAX_WBITS); switch (ret) { case Z_OK: return ptr_to_jlong(strm); case Z_MEM_ERROR: free(strm); JNU_ThrowOutOfMemoryError(env, 0); return jlong_zero; default: msg = ((strm->msg != NULL) ? strm->msg : (ret == Z_VERSION_ERROR) ? "zlib returned Z_VERSION_ERROR: " "compile time and runtime zlib implementations differ" : (ret == Z_STREAM_ERROR) ? "inflateInit2 returned Z_STREAM_ERROR" : "unknown error initializing zlib library"); free(strm); JNU_ThrowInternalError(env, msg); return jlong_zero; } } } JNIEXPORT void JNICALL Java_java_util_zip_Inflater_end(JNIEnv *env, jclass cls, jlong addr) { if (inflateEnd(jlong_to_ptr(addr)) == Z_STREAM_ERROR) { JNU_ThrowInternalError(env, 0); } else { free(jlong_to_ptr(addr)); //此處釋放堆外內存 } } 

參考



作者:黃小豆Jacob
鏈接:https://www.jianshu.com/p/5841df465eb9
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM