這個話題已是老生常談了,之因此又被我拎出來,是由於博主隔壁的一個童鞋最近寫了一篇叫作《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()
方法。
案例三
講這個例子前,你們對ThreadLocal
在Tomcat
中引發內存泄露有了解麼。不過,我要說一下,這個泄露問題,和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
可能用的不是工做線程池裏面的同一個線程,這會致使工做線程池裏面多個線程都會存在內存泄露。
另外,servlet
的doGet
方法裏面建立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
使用了線程池,線程是可能存在複用狀況。