【原創】淺談內存泄露

前言

這個話題已是老生常談了,之因此又被我拎出來,是由於博主隔壁的一個童鞋最近寫了一篇叫作《ThreadLocal內存泄露》的文章,我就不上連接了,由於寫的實在是。。(省略一萬字)
重點是寫完後,還被我問懵了。出於人道主義關懷,博主很不要臉的再寫一篇。java

正文

定義

首先,咱們要先談一下定義,由於一堆人搞不懂內存溢出和內存泄露的區別。
內存溢出(OutOfMemory):你只有十塊錢,我卻找你要了一百塊。對不起啊,我沒有這麼多錢。(給不起)
內存泄露(MemoryLeak):你有十塊錢,我找你要一塊。可是無恥的博主,不把錢還你了。(沒退還)
關係:屢次的內存泄露,會致使內存溢出。(博主不要臉的找你多要幾回錢,你就沒錢了,就是這個道理。)web

危害

ok,你們在項目中有沒遇到過java程序愈來愈卡的狀況。
由於內存泄露,會致使頻繁的Full GC,而Full GC 又會形成程序停頓,最後Crash了。所以,你會感受到你的程序愈來愈卡,愈來愈卡,而後你就被產品經理鄙視了。順便提一下,咱們之因此JVM調優,就是爲了減小Full GC的出現。
我記得,我曾經有一次,就遇到項目剛上線的時候好好的。結果隨着時間的堆積,報了OutOfMemoryError: PermGen space
說到這個PermGen space,忽然間,一陣洪荒之力,從博主體內噴涌而出,必定要介紹一下這個方法區,不過點到爲止,畢竟這不是在講《jvm從入門到放棄》。
方法區:出自java虛擬機規範, 可供各條線程共享運行時內存區域。它存儲了每個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容。
上面講的是規範,在不一樣虛擬機裏頭實現是不同的,最典型的就是永久代(PermGen space)元空間(Metaspace)面試

jdk1.8之前:實現方法區的叫永久代。由於在好久遠之前,java以爲類幾乎是靜態的,而且不多被卸載和回收,因此給了一個永久代的雅稱。所以,若是你在項目中,發現堆和永久代一直在不斷增加,沒有降低趨勢,回收的速度根本趕不上增加的速度,不用說了,這種狀況基本能夠肯定是內存泄露。算法

jdk1.8之後:實現方法區的叫元空間。Java以爲對永久代進行調優是很困難的。永久代中的元數據可能會隨着每一次Full GC發生而進行移動。而且爲永久代設置空間大小也是很難肯定的。所以,java決定將類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。這樣,咱們就避開了設置永久代大小的問題。可是,這種狀況下,一旦發生內存泄露,會佔用你的大量本地內存。若是你發現,你的項目中本地內存佔用率異常高。嗯,這就是內存泄露了。數據庫

如何排查

(1)經過jps查找java進程id。
(2)經過top -p [pid]發現內存佔用達到了最大值
(3)jstat -gccause pid 20000 每隔20秒輸出Full GC結果
(4)發現Full GC次數太多,基本就是內存泄露了。生成dump文件,藉助工具分析是哪一個對象太多了。基本能定位到問題在哪。apache

實例

在stackoverflow上,有一個問題,以下所示數組

I just had an interview, and I was asked to create a memory leak with Java. Needless to say I felt pretty dumb having no clue on how to even start creating one.tomcat

大體就是,由於面試須要手寫一段內存泄露的程序,而後提問的人忽然懵逼了,因而不少大佬紛紛給出回答。
案例一
此例子出自《算法》(第四版)一書,我簡化了一下app

class stack{    
        Object data[1000];    
        int top = 0;    
        public void push(Object o){        
            data[top++] = o;   
        }    
        public Object pop(Object o){ 
            return data[--top];
        }
    }

當數據從棧裏面彈出來以後,data數組還一直保留着指向元素的指針。那麼就算你把棧pop空了,這些元素佔的內存也不會被回收的。
解決方案就是less

public Object pop(Object o){ 
        Object result = data[--top];
        data[top] = null;
        return result;
    }

案例二
這個實際上是一堆例子,這些例子形成內存泄露的緣由都是相似的,就是不關閉流,具體的,能夠是文件流,socket流,數據庫鏈接流,等等
具體以下,沒關文件流

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

再好比,沒關閉鏈接

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

解決方案就是。。。嗯,你們應該都會。。你敢說你不會調close()方法。
案例三
講這個例子前,你們對ThreadLocalTomcat中引發內存泄露有了解麼。不過,我要說一下,這個泄露問題,和ThreadLocal自己關係不大,我看了一下官網給的例子,基本都是屬於使用不當引發的。
在Tomcat的官網上,記錄了這個問題。地址是:https://wiki.apache.org/tomcat/MemoryLeakProtection
不過,官網的這個例子,可能很差理解,咱們略做改動。

public class HelloServlet extends HttpServlet{
    private static final long serialVersionUID = 1L;

    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024 * 100];
    }

    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        localVariable.set(new LocalVariable());
    }
}

再來看下conf下sever.xml配置

<!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" 
        maxThreads="150" minSpareThreads="4"/>

線程池最大線程爲150個,最小線程爲4個
Tomcat中Connector組件負責接受並處理請求,每來一個請求,就會去線程池中取一個線程。
在訪問該servlet時,ThreadLocal變量裏面被添加了new LocalVariable()實例,可是沒有被remove,這樣該變量就隨着線程回到了線程池中。另外屢次訪問該servlet可能用的不是工做線程池裏面的同一個線程,這會致使工做線程池裏面多個線程都會存在內存泄露。

另外,servletdoGet方法裏面建立new LocalVariable()的時候使用的是webappclassloader
那麼
LocalVariable對象沒有釋放 -> LocalVariable.class沒有釋放 -> webappclassloader沒有釋放 -> webappclassloader加載的全部類也沒有被釋放,也形成了內存泄露。

除此以外,你在eclipse中,作一個reload操做,工做線程池裏面的線程仍是一直存在的,而且線程裏面的threadLocal變量並無被清理。而reload的時候,又會新構建一個webappclassloader,重複上述步驟。多reload幾回,就內存溢出。
不過Tomcat7.0之後,你每作一次reload,會清理工做線程池中線程的threadLocals變量。所以,這個問題在tomcat7.0後,不會存在。

ps:ThreadLocal的使用在Tomcat的服務環境下要注意,並不是每次web請求時候程序運行的ThreadLocal都是惟一的。ThreadLocal的什麼生命週期不等於一次Request的生命週期。ThreadLocal與線程對象緊密綁定的,因爲Tomcat使用了線程池,線程是可能存在複用狀況。

相關文章
相關標籤/搜索