南京網(wǎng)站的優(yōu)化石景山區(qū)百科seo
內(nèi)存泄漏:指的是不再使用的對(duì)象在系統(tǒng)中未被回收,內(nèi)存泄漏的積累可能會(huì)導(dǎo)致內(nèi)存溢出
什么是垃圾回收
Java中為了簡化對(duì)象的釋放,引入了自動(dòng)的垃圾回收(Garbage Collection
簡稱GC
)機(jī)制。通過垃
圾回收器來對(duì)不再使用的對(duì)象完成自動(dòng)的回收,垃圾回收器主要負(fù)責(zé)對(duì)【堆】上的內(nèi)存進(jìn)行回收。其他
很多現(xiàn)代語言比如C#、Python、Go
都擁有自己的垃圾回收器。
自動(dòng)垃圾回收 java
自動(dòng)根據(jù)對(duì)象是否使用由虛擬機(jī)來回收對(duì)象
? 優(yōu)點(diǎn):降低程序員實(shí)現(xiàn)難度、降低對(duì)象回收bug的可能性
? 缺點(diǎn):程序員無法控制內(nèi)存回收的及時(shí)性
手動(dòng)垃圾回收 C\C++
由程序員編程實(shí)現(xiàn)對(duì)象的刪除
? 優(yōu)點(diǎn):回收及時(shí)性高,由程序員把控回收的時(shí)機(jī)
? 缺點(diǎn):編寫不當(dāng)容易出現(xiàn)懸空指針、重復(fù)釋放、內(nèi)存泄漏等問題
Java虛擬機(jī)在運(yùn)行Java程序過程中管理的內(nèi)存區(qū)域,稱之為運(yùn)行時(shí)數(shù)據(jù)區(qū)。線程不共享的部分(程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū)),都是伴隨著線程的創(chuàng)建而創(chuàng)建,線程的銷毀而銷毀。而方法的棧幀在執(zhí)行完方法之后就會(huì)自動(dòng)彈出棧并釋放掉對(duì)應(yīng)的內(nèi)存。
一、方法區(qū)的回收
方法區(qū)中能回收的內(nèi)容主要就是不再使用的類。判定一個(gè)類可以被卸載,需要同時(shí)滿足三個(gè)條件:
-
此類所有實(shí)例對(duì)象都已被回收,在堆中不存在任何該類的實(shí)例對(duì)象以及子類對(duì)象。
-
加載該類的類加載器已被回收。
-
該類對(duì)應(yīng)的 java.lang.Class 對(duì)象沒有在任何地方被引用。
開發(fā)中此類場(chǎng)景一般很少出現(xiàn),主要在如
OSGi、JSP
的熱部署等應(yīng)用場(chǎng)景中。每個(gè)jsp文件對(duì)應(yīng)一個(gè)唯一的類加載器,當(dāng)一個(gè)jsp
文件修改了,就直接卸載這個(gè)jsp類加載器。重新創(chuàng)建類加載器,重新加載jsp文件。
手動(dòng)觸發(fā)回收
如果需要手動(dòng)觸發(fā)垃圾回收,可以調(diào)用System.gc()
方法。
語法: System.gc()
注意事項(xiàng):調(diào)用
System.gc()
方法并不一定會(huì)立即回收垃圾,僅僅是向Java虛擬機(jī)
發(fā)送一個(gè)垃圾回收的請(qǐng)求,具體是否需要執(zhí)行垃圾回收J(rèn)ava虛擬機(jī)會(huì)自行判斷。
二、堆回收
1. 如何判斷堆上的對(duì)象可以回收?
Java中的對(duì)象是否能被回收,是根據(jù)對(duì)象是否被引用來決定的。
如果對(duì)象被引用了,說明該對(duì)象還在使用,不允許被回收。
圖中A的實(shí)例對(duì)象要回收,有兩個(gè)引用要去除:
- 棧中
a1
變量到對(duì)象的引用 2.B
對(duì)象到A
對(duì)象的引用
即a1 = null; b1.a = null;
2. 如何判斷堆上的對(duì)象沒有被引用?
常見的有兩種方法:引用計(jì)數(shù)法、可達(dá)性分析法
2.1 引用計(jì)數(shù)法
會(huì)為每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù)器,當(dāng)對(duì)象被引用時(shí)加1,取消引用時(shí)減1。
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,C++中的智能指針就采用了引用計(jì)數(shù)法
缺點(diǎn):
1.每次引用和取消引用都需要維護(hù)計(jì)數(shù)器,對(duì)系統(tǒng)性能會(huì)有一定的影響
2.存在循環(huán)引用問題,所謂循環(huán)引用就是當(dāng)A引用B,B同時(shí)引用A時(shí)會(huì)出現(xiàn)對(duì)象無法回收的問題。
查看垃圾回收日志,可以使用-verbose:gc參數(shù)。
語法: -verbose:gc
2.2 可達(dá)性分析法
Java使用的是可達(dá)性分析算法來判斷對(duì)象是否可以被回收。可達(dá)性分析將對(duì)象分為兩類:垃圾回收的根對(duì)象(GC Root
)和普通對(duì)象,對(duì)象與對(duì)象之間存在引用關(guān)系。
下圖中A到B再到C和D,形成了一個(gè)引用鏈,可達(dá)性分析算法指的是如果從某個(gè)到GC Root對(duì)象是可達(dá)的,對(duì)象就不可被回收。
哪些對(duì)象被稱之為GC Root
對(duì)象呢?
1)線程Thread對(duì)象。引用線程棧幀中的方法參數(shù)、局部變量等
2)系統(tǒng)類加載器加載的java.lang.Class
對(duì)象。引用類中的靜態(tài)變量
3)監(jiān)視器對(duì)象,用來保存同步鎖synchronized
關(guān)鍵字持有的對(duì)象。
4)本地方法調(diào)用時(shí)使用的全局對(duì)象。
查看GC Root
通過arthas
和eclipse Memory Analyzer (MAT)
工具可以查看GC Root
,MAT
工具是eclipse
推出的Java堆內(nèi)存
檢測(cè)工具。具體操作步驟如下:
1、使用arthas
的heapdump
命令將堆內(nèi)存快照保存到本地磁盤中。
2、使用MAT
工具打開堆內(nèi)存快照文件。
3、選擇GC Roots
功能查看所有的GC Root
。
2.3 幾種常見的對(duì)象引用
可達(dá)性算法中描述的對(duì)象引用,一般指的是強(qiáng)引用,即是GCRoot
對(duì)象對(duì)普通對(duì)象有引用關(guān)系,只要這層關(guān)系存在,普通對(duì)象就不會(huì)被回收。
除了強(qiáng)引用之外,Java中還設(shè)計(jì)了幾種其他引用方式:軟引用、弱引用、虛引用、終結(jié)器引用
1)軟引用:
相對(duì)于強(qiáng)引用是一種比較弱的引用關(guān)系,如果一個(gè)對(duì)象只有軟引用關(guān)聯(lián)到它,當(dāng)程序內(nèi)存不足時(shí),就會(huì)將軟引用中的數(shù)據(jù)進(jìn)行回收。在JDK 1.2
版之后提供了SoftReference
類來實(shí)現(xiàn)軟引用,軟引用常用于緩存中
軟引用的執(zhí)行過程如下:
1 將對(duì)象使用軟引用包裝起來,new SoftReference<對(duì)象類型>(對(duì)象)
。
2 內(nèi)存不足時(shí),虛擬機(jī)嘗試進(jìn)行垃圾回收。
3 如果垃圾回收仍不能解決內(nèi)存不足的問題,回收軟引用中的對(duì)象。
4 如果依然內(nèi)存不足,拋出OutOfMemory
異常。
軟引用中的對(duì)象如果在內(nèi)存不足時(shí)回收,SoftReference
對(duì)象本身也需要被回收。如何知道哪些SoftReference
對(duì)象需要回收呢?SoftReference
提供了一套隊(duì)列機(jī)制:
1 軟引用創(chuàng)建時(shí),通過構(gòu)造器傳入引用隊(duì)列
2 在軟引用中包含的對(duì)象被回收時(shí),該軟引用對(duì)象會(huì)被放入引用隊(duì)列
3 通過代碼遍歷引用隊(duì)列,將SoftReference
的強(qiáng)引用刪除
2)弱引用:
整體機(jī)制和軟引用基本一致,區(qū)別在于弱引用包含的對(duì)象在垃圾回收時(shí),不管內(nèi)存夠不夠都會(huì)直接被回收。在JDK 1.2
版之后提供了WeakReference
類來實(shí)現(xiàn)弱引用,弱引用主要在ThreadLocal
中使用。弱引用對(duì)象本身也可以使用引用隊(duì)列進(jìn)行回收。
3)虛引用和終結(jié)器引用
- 這兩種引用在常規(guī)開發(fā)中不會(huì)使用到,僅了解。
- 虛引用也叫幽靈引用/幻影引用,不能通過虛引用對(duì)象獲取到包含的對(duì)象。虛引用唯一的用途是當(dāng)對(duì)象被垃圾回收器回收時(shí)可以接收到對(duì)應(yīng)的通知。Java中使用
PhantomReference
實(shí)現(xiàn)了虛引用,直接內(nèi)存中為了及時(shí)知道直接內(nèi)存對(duì)象不再使用,從而回收內(nèi)存,使用了虛引用來實(shí)現(xiàn)。 - 終結(jié)器引用指的是在對(duì)象需要被回收時(shí),終結(jié)器引用會(huì)關(guān)聯(lián)對(duì)象并放置在
Finalizer
類中的引用隊(duì)列中,在稍后由一條由FinalizerThread
線程從隊(duì)列中獲取對(duì)象,然后執(zhí)行對(duì)象的finalize
方法,在對(duì)象第二次被回收時(shí),該對(duì)象才真正的被回收。在這個(gè)過程中可以在finalize
方法中再將自身對(duì)象使用強(qiáng)引用關(guān)聯(lián)上,但是不建議這樣做。
三、垃圾回收算法
垃圾回收要做的有兩件事:
1)找到內(nèi)存中存活的對(duì)象
2)釋放不再存活對(duì)象的內(nèi)存,使得程序能再次利用這部分空間
四種算法:標(biāo)記-清除算法、復(fù)制算法、標(biāo)記-整理算法、分代GC
垃圾回收算法的評(píng)價(jià)標(biāo)準(zhǔn)
Java垃圾回收過程會(huì)通過單獨(dú)的GC線程來完成,但是不管使用哪一種GC算法,都會(huì)有部分階段需要停止所有的用戶線程。這個(gè)過程被稱之為
Stop The World
簡稱STW
,如果STW時(shí)間過長則會(huì)影響用戶的使用。
判斷GC算法是否優(yōu)秀,可以從三個(gè)方面來考慮:
1)吞吐量:指的是 CPU 用于執(zhí)行用戶代碼的時(shí)間
與 CPU 總執(zhí)行時(shí)間的比值
即吞吐量
= 執(zhí)行用戶代碼時(shí)間
/(執(zhí)行用戶代碼時(shí)間 + GC時(shí)間)
。吞吐量數(shù)值越高,垃圾回收的效率就越高。比如:虛擬機(jī)總共運(yùn)行了 100 分鐘,其中GC花掉 1 分鐘,那么吞吐量就是 99%
2)最大暫停時(shí)間:最大暫停時(shí)間指的是所有在垃圾回收過程中的STW時(shí)間最大值
。最大暫停時(shí)間越短,用戶使用系統(tǒng)時(shí)受到的影響就越短。
比如如下的圖中,黃色部分的STW就是最大暫停時(shí)間,顯而易見上面的圖比下面的圖擁有更少的最大暫停時(shí)間。
3)堆使用效率:不同垃圾回收算法,對(duì)堆內(nèi)存的使用方式是不同的。比如標(biāo)記清除算法,可以使用完整的堆內(nèi)存。而復(fù)制算法會(huì)將堆內(nèi)存一分為二,每次只能使用一半內(nèi)存。從堆使用效率上來說,標(biāo)記清除算法要優(yōu)于復(fù)制算法。
上述三種評(píng)價(jià)標(biāo)準(zhǔn):堆使用效率、吞吐量,以及最大暫停時(shí)間不可兼得。
一般來說,堆內(nèi)存越大,最大暫停時(shí)間就越長。想要減少最大暫停時(shí)間,就會(huì)降低吞吐量。
不同的垃圾回收算法,適用于不同的場(chǎng)景
1. 標(biāo)記清除算法
標(biāo)記清除算法的核心思想分為兩個(gè)階段
1)標(biāo)記階段,將所有存活的對(duì)象進(jìn)行標(biāo)記。Java中使用可達(dá)性分析算法,從GC Root
開始通過引用鏈遍歷出所有存活對(duì)象。
2)清除階段,從內(nèi)存中刪除沒有被標(biāo)記也就是非存活對(duì)象
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,只需要在第一階段給每個(gè)對(duì)象維護(hù)標(biāo)志位,第二階段刪除對(duì)象即可。
缺點(diǎn):1)碎片化問題:由于內(nèi)存是連續(xù)的,所以在對(duì)象被刪除之后,內(nèi)存中會(huì)出現(xiàn)很多細(xì)小的可用內(nèi)存單元。如果我們需要的是一個(gè)比較大的空間,很有可能這些內(nèi)存單元的大小過小無法進(jìn)行分配。
2)分配速度慢:由于內(nèi)存碎片的存在,需要維護(hù)一個(gè)空閑鏈表,極有可能發(fā)生每次需要遍歷到鏈表的最后才能獲得合適的內(nèi)存空間。
2. 復(fù)制算法
復(fù)制算法的核心思想是:
1)準(zhǔn)備兩塊空間From
空間和To
空間,每次在對(duì)象分配階段,只能使用其中一塊空間(From
空間)。
2)在垃圾回收GC階段,將From
中存活對(duì)象復(fù)制到To
空間。
3)將兩塊空間的From
和To
名字互換。
完整的復(fù)制算法的例子:
1.將堆內(nèi)存分割成兩塊From
空間To
空間,對(duì)象分配階段,創(chuàng)建對(duì)象。
2.GC
階段開始,將GC Root
搬運(yùn)到To
空間
3.將GC Root
關(guān)聯(lián)的對(duì)象,搬運(yùn)到To
空間
4.清理From
空間,并把名稱互換
優(yōu)點(diǎn):
1)吞吐量高:復(fù)制算法只需要遍歷一次存活對(duì)象復(fù)制到To
空間即可,比標(biāo)記-整理算法少了一次遍歷的過程,因而性能較好,但是不如標(biāo)記-清除算法,因?yàn)闃?biāo)記清除算法不需要進(jìn)行對(duì)象的移動(dòng)
2)不會(huì)發(fā)生碎片化:復(fù)制算法在復(fù)制之后就會(huì)將對(duì)象按順序放入To
空間中,所以對(duì)象以外的區(qū)域都是可用空間,不存在碎片化內(nèi)存空間。
缺點(diǎn):內(nèi)存使用效率低,每次只能讓一半的內(nèi)存空間來為創(chuàng)建對(duì)象使用
3. 標(biāo)記整理算法
標(biāo)記整理算法也叫標(biāo)記壓縮算法,是對(duì)標(biāo)記清理算法中容易產(chǎn)生內(nèi)存碎片問題的一種解決方案。
核心思想分為兩個(gè)階段:
1)標(biāo)記階段,將所有存活的對(duì)象進(jìn)行標(biāo)記。Java中使用可達(dá)性分析算法,從GC Root
開始通過引用鏈遍歷出所有存活對(duì)象。
2)整理階段,將存活對(duì)象移動(dòng)到堆的一端。清理掉存活對(duì)象的內(nèi)存空間。
優(yōu)點(diǎn):
1)整個(gè)堆內(nèi)存都可以使用,不會(huì)像復(fù)制算法只能使用半個(gè)堆內(nèi)存
2)不會(huì)發(fā)生碎片化,在整理階段可以將對(duì)象往內(nèi)存的一側(cè)進(jìn)行移動(dòng),剩下的空間都是可以分配對(duì)象的有效空間
缺點(diǎn):整理階段的效率不高。整理算法有很多種,比如Lisp2
整理算法需要對(duì)整個(gè)堆中的對(duì)象搜索3次,整體性能不佳??梢酝ㄟ^Two Finger
、表格算法
、ImmixGC
等高效的整理算法優(yōu)化此階段的性能
4. 分代垃圾回收算法
分代垃圾回收將整個(gè)內(nèi)存區(qū)域劃分為年輕代和老年代:年輕代(新生代) Young區(qū)存放存活時(shí)間比較短的對(duì)象,Old區(qū)老年代存放存活時(shí)間比較長的對(duì)象.
1)分代回收時(shí),創(chuàng)建出來的對(duì)象,首先會(huì)被放入Eden
伊甸園區(qū)。隨著對(duì)象在Eden
區(qū)越來越多,如果Eden
區(qū)滿,新創(chuàng)建的對(duì)象已經(jīng)無法放入,就會(huì)觸發(fā)年輕代的GC,稱為Minor GC
或者Young GC
。Minor GC
會(huì)把eden
區(qū)和From
區(qū)(S0
)中需要回收的對(duì)象回收,把沒有回收的對(duì)象放入To
區(qū)(S1
)。
2)接下來,S0
會(huì)變成To
區(qū),S1變成From區(qū)。當(dāng)eden
區(qū)滿時(shí)再往里放入對(duì)象,依然會(huì)發(fā)生Minor GC
。此時(shí)會(huì)回收eden
區(qū)和S1(from)
中的對(duì)象,并把eden
和from
區(qū)中剩余的對(duì)象放入S0
。每次Minor GC
中都會(huì)為對(duì)象記錄他的年齡,初始值為0,每次GC完加1。
3)如果Minor GC
后對(duì)象的年齡達(dá)到閾值(最大15,默認(rèn)值和垃圾回收器有關(guān)),對(duì)象就會(huì)被晉升至老年代。當(dāng)老年代中空間不足,無法放入新的對(duì)象時(shí),先嘗試minor gc
如果還是不足,就會(huì)觸發(fā)Full GC
,Full GC
會(huì)對(duì)整個(gè)堆進(jìn)行垃圾回收。如果Full GC
依然無法回收掉老年代的對(duì)象,那么當(dāng)對(duì)象繼續(xù)放入老年代時(shí),就會(huì)拋出Out Of Memory
異常。
下圖中的程序?yàn)槭裁磿?huì)出現(xiàn)OutOfMemory?
從上圖可以看到,Full GC
無法回收掉老年代的對(duì)象,那么當(dāng)對(duì)象繼續(xù)放入老年代時(shí),就會(huì)拋出Out Of Memory
異常。
arthas查看分代之后的內(nèi)存情況:
在JDK8
中,添加-XX:+UseSerialGC
參數(shù)使用分代回收的垃圾回收器,運(yùn)行程序。
在arthas
中使用memory
命令查看內(nèi)存,顯示出三個(gè)區(qū)域的內(nèi)存情況。 圖2
調(diào)整內(nèi)存區(qū)域的大小:通過添加jvm啟動(dòng)參數(shù)修改各個(gè)區(qū)域大小和比例。注意加上-XX:+UseSerialGC
為什么分代GC算法要把堆分成年輕代和老年代?
- 系統(tǒng)中的大部分對(duì)象,都是創(chuàng)建出來之后很快就不再使用可以被回收,比如用戶獲取訂單數(shù)據(jù),訂單數(shù)據(jù)返回給用戶之后就可以釋放了。
- 老年代中會(huì)存放長期存活的對(duì)象,比如
Spring
的大部分bean
對(duì)象,在程序啟動(dòng)之后就不會(huì)被回收了。- 在虛擬機(jī)的默認(rèn)設(shè)置中,新生代大小要遠(yuǎn)小于老年代的大小。
答:分代GC算法將堆分成年輕代和老年代主要原因有:
1.可以通過調(diào)整年輕代和老年代的比例來適應(yīng)不同類型的應(yīng)用程序,提高內(nèi)存的利用率和性能。
2)新生代和老年代使用不同的垃圾回收算法,新生代一般選擇“復(fù)制算法”,老年代可以選擇“標(biāo)記-清除”和“標(biāo)記-整理”算法,由程序員來選擇靈活度較高。
3)分代的設(shè)計(jì)中允許只回收新生代(minor gc
),如果能滿足對(duì)象分配的要求就不需要對(duì)整個(gè)堆進(jìn)行回收(full gc
),STW
時(shí)間就會(huì)減少。