GZIPInputStream 流未關閉引起的內存泄漏問題


近日線上一個項目總是時隔1周發生OOM自動重啟,問題很明顯內存泄漏了。。。

使用jmap查看一下線上服務堆使用情況,實例最多的前10個類

110125 instances of class [C 
108705 instances of class java.lang.String 
88066 instances of class 
java.util.concurrent.ConcurrentHashMap$Node 
79224 instances of class java.lang.Object 
52984 instances of class [B 
48482 instances of class java.lang.ref.Finalizer 
39684 instances of class java.util.zip.Inflater  <---罪魁禍手
39684 instances of class java.util.zip.ZStreamRef 
28168 instances of class [Ljava.lang.Object; 
26576 instances of class java.util.HashMap$Node 

看到這個類排名第一反應就是GZIP相關的操作可能有問題,那么我們目光聚集到代碼上吧

public static String unZip(String str) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] bytes = Base64.getDecoder().decode(str);
    ByteArrayInputStream in = new ByteArrayInputStream(bytes);
    GZIPInputStream gzip = new GZIPInputStream(in);
    byte[] buffer = new byte[256];
    int n = 0;
    while ((n = gzip.read(buffer)) >= 0) {
        out.write(buffer, 0, n);
    }
    return out.toString(CODE);
}

這段代碼是當時想要使用GZIP做解壓縮從網上抄來了,當時只是用單測驗證了一下這段代碼的正確性,就上線了。

出現了內存泄漏問題之后,回過頭來反思這段代碼發現這里使用了3個流ByteArrayOutputStream ,ByteArrayInputStream ,GZIPInputStream

重點是這三個流在代碼結束之后都沒有關閉!!!
依次點開三個流的close()方法看了下
ByteArrayOutputStream ,ByteArrayInputStream 這兩個流的close()方法其實是空的,說明這兩個流其實關閉與否都沒有關系。

GZIPInputStream 的close()方法

public void close() throws IOException {
    if (!closed) {
        super.close();
        eos = true;
        closed = true;
    }
}

看到這個方法后,具體怎么關閉的其實不那么重要了,重要的是說明了這個流是需要關閉的

現在我們再看看內存泄漏的具體原因是什么吧,我們依次點開GZIPInputStream 的構造方法

public GZIPInputStream(InputStream in, int size) throws IOException {
    super(in, new Inflater(true), size); //看到堆內大量實例Inflater了
    usesDefaultInflater = true;
    readHeader(in);
}

點開Inflater的構造方法

public Inflater(boolean nowrap) {
    zsRef = new ZStreamRef(init(nowrap)); //c++方法init
}

這個init方法使用了C++ calloc申請內存,這部分內存是無法被Java GC回收的,這就導致我們的服務可用堆內存越來越小,最后程序OOM Crash重啟

修改這段代碼很簡單,所有流使用完畢之后關閉就好了,最終代碼如下

private static String unZip(String str) throws IOException {
    try (ByteArrayOutputStream out = new ByteArrayOutputStream();
        ByteArrayInputStream in = new ByteArrayInputStream(Base64.getDecoder().decode(str));
        GZIPInputStream gzip = new GZIPInputStream(in)) {
        byte[] buffer = new byte[256];
        int n = 0;
        while ((n = gzip.read(buffer)) >= 0) {
            out.write(buffer, 0, n);
        }
        return out.toString(CODE);
    }
}

雖然ByteArrayOutputStream ByteArrayInputStream 這兩個流的close()方法為空,無需關閉。但是從這個線上問題得出的反思,任何流,使用完畢之后一定要注意養成關閉的好習慣。(除非需要復用流)


免責聲明!

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



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