Java超詳細(xì)分析垃圾回收機(jī)制
前言
在前面我們對(duì)類加載, 運(yùn)行時(shí)數(shù)據(jù)區(qū) ,執(zhí)行引擎等作了詳細(xì)的介紹 , 這節(jié)我們來(lái)看另一重點(diǎn) : 垃圾回收.
垃圾回收概述
垃圾回收是java的招牌能力 ,極大的提高了開(kāi)發(fā)效率, java是自動(dòng)化的垃圾回收, 其他語(yǔ)言有的則需要程序員手動(dòng)回收 , 那么什么是垃圾呢?
垃圾是指在運(yùn)行程序中沒(méi)有任何引用指向的對(duì)象,這個(gè)對(duì)象就是需要被回收的垃圾。如果不及時(shí)對(duì)內(nèi)存中的垃圾進(jìn)行清理,那么,這些垃圾對(duì)象所占的內(nèi)存空間會(huì)一直保留到應(yīng)用程序結(jié)束,被保留的空間無(wú)法被其他對(duì)象使用。甚至可能導(dǎo)致內(nèi)存溢出。
在早期C/C++時(shí)代, 需要手動(dòng)回收垃圾, 如果一旦疏忽, 還會(huì)導(dǎo)致內(nèi)存泄漏問(wèn)題
這里引出了兩個(gè)名詞 , 內(nèi)存溢出和內(nèi)存泄漏, 先來(lái)解釋這兩個(gè)意思
內(nèi)存溢出和內(nèi)存泄漏
內(nèi)存溢出 : 內(nèi)存被占滿, 內(nèi)存不夠用了
內(nèi)存泄漏 : 程序中存在不被使用的對(duì)象(但GC無(wú)法判定它們?yōu)槔?, GC(垃圾回收)無(wú)法去收集清理它們, 這就導(dǎo)致這塊空間一直被占用, 無(wú)法釋放出來(lái),這就是內(nèi)存泄漏
一些提供 close() 的對(duì)象等, 例如在JDBC中 Connection 沒(méi)有去關(guān)閉等, 這樣的越積越多就導(dǎo)致內(nèi)存泄漏問(wèn)題 , 內(nèi)存泄漏越來(lái)越多最終會(huì)導(dǎo)致內(nèi)存溢出問(wèn)題(泄漏逐漸蠶食內(nèi)存)
實(shí)際情況很多時(shí)候一些不太好的實(shí)踐(或疏忽)會(huì)導(dǎo)致對(duì)象的生命周期變得很長(zhǎng)甚至導(dǎo)致OOM,也可以叫做寬泛意義上的“內(nèi)存泄漏”。
在單例模式下, 單例的生命周期和程序一樣長(zhǎng),如果持有對(duì)外部對(duì)象的引用的話,那么這個(gè)外部對(duì)象是不能被回收的,則會(huì)導(dǎo)致內(nèi)存泄漏的產(chǎn)生
那么GC主要關(guān)心哪塊區(qū)域的收集呢?
在前面運(yùn)行時(shí)數(shù)據(jù)區(qū)我們說(shuō)過(guò) , 可以總結(jié)如下 : 頻繁回收新生區(qū), 較少回收老年區(qū) , 基本不收集方法區(qū)(元空間)
垃圾回收算法
在垃圾回收時(shí), 分為兩個(gè)階段, 標(biāo)記階段和回收階段 , 這兩個(gè)階段使用了不同的算法思想來(lái)區(qū)分垃圾 , 我們來(lái)依次論述
標(biāo)記階段
想要清除垃圾, 我們先得了解什么是垃圾 , 那么如何來(lái)判斷一個(gè)對(duì)象是否是垃圾呢?簡(jiǎn)單來(lái)說(shuō),當(dāng)一個(gè)對(duì)象已經(jīng)不再被任何的存活對(duì)象繼續(xù)引用時(shí), 就已經(jīng)是垃圾了
標(biāo)記階段有兩種算法 : 引用計(jì)數(shù)算法和可達(dá)性分析算法
引用計(jì)數(shù)算法
這個(gè)算法思想比較簡(jiǎn)單 , 就是使用一個(gè)計(jì)數(shù)器, 如果有一個(gè)引用指向這個(gè)對(duì)象, 那么計(jì)數(shù)器就加1 , 引用失效計(jì)數(shù)器就減一 . 計(jì)數(shù)器為0 則表示該對(duì)象可回收
但是這個(gè)算法有一個(gè)嚴(yán)重的問(wèn)題, 此問(wèn)題也導(dǎo)致我們現(xiàn)如今已不再使用此算法 : 無(wú)法處理循環(huán)依賴問(wèn)題 , 這是一個(gè)致命缺陷 , 什么是循環(huán)依賴問(wèn)題呢 ?
如圖 , 引用P 指向?qū)ο驛 , 而對(duì)象A又指向?qū)ο驜, 對(duì)象B又指向?qū)ο驝 , 對(duì)象C繼續(xù)指向?qū)ο驛, 此時(shí)將引用 P 置null , 此時(shí)這三個(gè)對(duì)象形成了依賴閉環(huán), 但都沒(méi)有直接的引用去指向它們, 這時(shí)如果采用引用計(jì)數(shù)算法, 這三個(gè)都不為0 , 也就無(wú)法被回收,出現(xiàn)內(nèi)存泄漏問(wèn)題
所以在這種條件下, 我們提出了
可達(dá)性分析算法(根搜索算法、追蹤性垃圾收集)
相對(duì)于引用計(jì)數(shù)算法而言,可達(dá)性分析算法不僅同樣具備實(shí)現(xiàn)簡(jiǎn)單和執(zhí)行高效等特點(diǎn),更重要的是該算法可以有效地解決在引用計(jì)數(shù)算法中循環(huán)引用的問(wèn)題 ,防止內(nèi)存泄漏的發(fā)生。
基本思路如下 :
1.可達(dá)性分析算法是以根對(duì)象(GCRoots)為起始點(diǎn),按照從上至下的方式搜索被根對(duì)象集合所連接的目標(biāo)對(duì)象是否可達(dá)。
2.使用可達(dá)性分析算法后,內(nèi)存中的存活對(duì)象都會(huì)被根對(duì)象集合直接或間接連接著,搜索所走過(guò)的路徑稱為引用鏈(Reference Chain)
3.如果目標(biāo)對(duì)象沒(méi)有任何引用鏈相連,則是不可達(dá)的,就意味著該對(duì)象己經(jīng)死亡, 可以標(biāo)記為垃圾對(duì)象。
4.在可達(dá)性分析算法中,只有能夠被根對(duì)象集合直接或者間接連接的對(duì)象才是存活對(duì)象。
也就是從根對(duì)象往下開(kāi)始搜索 , 如果目標(biāo)對(duì)象不存在引用鏈 ,則判斷可以回收
public static void main(String[] args) { List<Integer> list = new ArrayList(); while(true){ list.add(new Random().nextInt()); } }
以上程序, list指向的對(duì)象作為根對(duì)象, 死循環(huán)生成的每個(gè)隨機(jī)數(shù)都存在引用鏈, 所以此程序最終會(huì)導(dǎo)致內(nèi)存溢出
public static void main(String[] args) { while(true){ Random r = new Random(); } }
上面的雖然存在引用指向, 但每次循環(huán)引用都會(huì)改變指向, 也就不存在引用鏈 , 所以每次都會(huì)被回收, 不會(huì)導(dǎo)致無(wú)法回收的問(wèn)題
那么哪些對(duì)象可以作為根對(duì)象呢?
1.虛擬機(jī)棧中引用的對(duì)象 比如:各個(gè)線程被調(diào)用的方法中使用到的參數(shù)、局部變量等。
2.本地方法棧內(nèi) JNI(通常說(shuō)的本地方法)引用的對(duì)象
3.方法區(qū)中類靜態(tài)屬性引用的對(duì)象,比如:Java 類的引用類型靜態(tài)變量
4.方法區(qū)中常量引用的對(duì)象,比如:字符串常量池(StringTable)里的引用
5.所有被同步鎖 synchronized 持有的對(duì)象
6.Java 虛擬機(jī)內(nèi)部的引用。
7.基 本 數(shù) 據(jù) 類 型 對(duì) 應(yīng) 的 Class 對(duì) 象 ,一些常駐 的 異 常 對(duì) 象 ( 如 :NullPointerException、OutofMemoryError),系統(tǒng)類加載器。
總結(jié)就是 : 棧, 方法區(qū) ,字符串常量池等地方對(duì)堆空間進(jìn)行引用的,都可以作為 GC Roots 進(jìn)行可達(dá)性分析
以上可作為根對(duì)象的都有這樣的特點(diǎn) : 活躍, 不可變,存活時(shí)間長(zhǎng), 在程序中至關(guān)重要. 例如: 靜態(tài)成員等, 同步鎖持有的對(duì)象等, 這些都是不能被隨意回收的
另外在可達(dá)性分析算法枚舉根節(jié)點(diǎn)(root 對(duì)象)時(shí)會(huì)產(chǎn)生STW(Stop-the-World) , 關(guān)于STW , 我們下面來(lái)介紹
STW(Stop-the-World)
指的是 GC 事件發(fā)生過(guò)程中,會(huì)產(chǎn)生應(yīng)用程序的停頓。停頓產(chǎn)生時(shí)整個(gè)應(yīng)用程序線程都會(huì)被暫停,沒(méi)有任何響應(yīng),有點(diǎn)像卡死的感覺(jué),這個(gè)停頓稱為 STW。
我們?cè)俅位氐缴厦娴膯?wèn)題 , 執(zhí)行可達(dá)性分析算法為什么需要停頓所有java執(zhí)行線程呢(STW)?
因?yàn)閷?duì)象的狀態(tài)是不停變化了 , 如果在我們確定哪個(gè)對(duì)象是垃圾的時(shí)候, 此對(duì)象的狀態(tài)還在不停變化時(shí), 這樣是沒(méi)法分析的 , 此時(shí)我們?nèi)シ治瞿鼙3忠恢滦缘囊粋€(gè)快照(某一時(shí)間點(diǎn)的執(zhí)行狀態(tài)) ,從而得到一個(gè)比較準(zhǔn)確的結(jié)果
需要注意的是, STW是無(wú)法避免的 , 和采用哪款GC也無(wú)關(guān), 我們只能去盡量減少停頓的時(shí)間,STW 是 JVM 在后臺(tái)自動(dòng)發(fā)起和自動(dòng)完成的。在用戶不可見(jiàn)的情況下,把用戶正常的工作線程全部停掉。
了解了這些, 接著我們來(lái)看回收階段的算法
回收階段
當(dāng)GC識(shí)別了垃圾之后, 接著就是垃圾回收了, 這里采用了 3 種不同的算法,接著來(lái)介紹
標(biāo)記-清除算法
顧名思義, 包括兩個(gè)階段 : 標(biāo)記和清除, 不過(guò)此處的標(biāo)記和垃圾標(biāo)記階段的標(biāo)記可是不同的
標(biāo)記:Collector 從引用根節(jié)點(diǎn)開(kāi)始遍歷,標(biāo)記所有被引用的對(duì)象。一般是在對(duì)象的Header 中記錄為可達(dá)對(duì)象。(注意:標(biāo)記的是被引用的對(duì)象,也就是可達(dá)對(duì)象,并非標(biāo)記的是即將被清除的垃圾對(duì)象)。
清除:Collector 對(duì)堆內(nèi)存從頭到尾進(jìn)行線性的遍歷,如果發(fā)現(xiàn)某個(gè)對(duì)象在其 Header 中沒(méi)有標(biāo)記為可達(dá)對(duì)象,則將其回收。
圖示如下 :
這里也需要注意, 此清除也不是簡(jiǎn)單的清除, 發(fā)現(xiàn)了垃圾對(duì)象后, 會(huì)先維護(hù)一個(gè)空列表用來(lái)記錄垃圾的地址,下次有新對(duì)象需要加載時(shí),判斷垃圾的位置空間是否夠,如果夠,就存放(也就是覆蓋原有的地址)。
標(biāo)記 - 清除算法比較基礎(chǔ)容易理解, 另外它也有很多缺點(diǎn) , 例如效率不高, GC時(shí)存在STW . 另外可以注意到, 這樣做會(huì)造成空間不是連續(xù)的(空間碎片化) , 此時(shí)就需要一個(gè)空列表來(lái)記錄這些地址
復(fù)制算法
為了解決標(biāo)記 - 清除算法在效率方面的缺陷 , 復(fù)制算法采用將內(nèi)存按容量劃分的方式, 劃分成大小相等的兩塊 , 每次只使用其中的一塊. 算法思想如下 :
將正在使用的存活對(duì)象全部復(fù)制到另一塊未被使用空間 , 擺放整齊 , 然后清空此空間所有對(duì)象
復(fù)制算法優(yōu)點(diǎn)是 : 簡(jiǎn)單高效, 不會(huì)出現(xiàn)"碎片"問(wèn)題
缺點(diǎn)當(dāng)然也很明顯 : 需要兩倍的內(nèi)存空間 , 開(kāi)銷較大 , 另外GC如果采用 G1 垃圾回收器的話 , 它將空間拆成了很多份, 如果采用復(fù)制算法, 還需要維護(hù)各區(qū)之間的關(guān)系
對(duì)于復(fù)制算法的思想而言, 如果對(duì)老年區(qū)采用此算法, 老年區(qū)對(duì)象較多,存活周期較長(zhǎng), 這時(shí)效率就會(huì)有點(diǎn)低 , 所以復(fù)制算法大多用于 young 區(qū), 幸存者0 區(qū)和幸存者1 區(qū)之間的相互轉(zhuǎn)換中
標(biāo)記-壓縮算法
上面我們說(shuō)過(guò), 復(fù)制算法相對(duì)于老年區(qū)來(lái)說(shuō), 效率就有點(diǎn)低了 , 所以針對(duì)老年區(qū)的回收, 就采用了標(biāo)記 - 壓縮算法 , 標(biāo)記 - 清除算法雖然也可以應(yīng)用于老年區(qū), 但是效率低下, 容易產(chǎn)生內(nèi)存碎片
算法思想 :
第一階段和標(biāo)記清除算法一樣,從根節(jié)點(diǎn)開(kāi)始標(biāo)記所有被引用對(duì)象
第二階段將所有的存活對(duì)象壓縮到內(nèi)存的一端,按順序排放。之后,清理邊界外所有的空間。
標(biāo)記-壓縮算法的最終效果等同于標(biāo)記-清除算法執(zhí)行完成后,再進(jìn)行一次內(nèi)存碎片整理,因此,也可以把它稱為標(biāo)記-清除-壓縮(Mark-Sweep-Compact)算法 , 標(biāo)記- 壓縮是移動(dòng)式的 , 將對(duì)象在內(nèi)存中依次排列比維護(hù)一個(gè)空列表少了不少開(kāi)銷(如果對(duì)象排列整齊,當(dāng)我們需要給新對(duì)象分配內(nèi)存時(shí),JVM 只需要持有一個(gè)內(nèi)存的起始地址即可)
優(yōu)點(diǎn) : 相對(duì)于標(biāo)記 -清除算法避免了內(nèi)存碎片化 , 相對(duì)于復(fù)制算法, 避免開(kāi)辟額外的空間
缺點(diǎn) : 從效率上來(lái)說(shuō)是不如復(fù)制算法的 , 移動(dòng)時(shí), 如果存在對(duì)象相互引用, 則需要調(diào)整引用的位置, 另外移動(dòng)過(guò)程中也會(huì)有STW
三種算法的比較
復(fù)制算法是效率最高的 , 但是花費(fèi)空間最大
標(biāo)記 - 壓縮算法雖然較為兼顧 , 但效率也變低, 比標(biāo)記- 清除多了個(gè)整理內(nèi)存的過(guò)程, 比復(fù)制算法多了標(biāo)記的過(guò)程
總結(jié)
到此關(guān)于 jvm 的大部分已經(jīng)講述完了, 在后續(xù)會(huì)再補(bǔ)充兩個(gè)部分 : 對(duì)象的finalize() 方法機(jī)制和對(duì)象的引用 ,感謝您的閱讀與關(guān)注 ,謝謝 !!!
到此這篇關(guān)于Java超詳細(xì)分析垃圾回收機(jī)制的文章就介紹到這了,更多相關(guān)Java垃圾回收內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mybatis?plus中如何編寫(xiě)sql語(yǔ)句
這篇文章主要介紹了mybatis?plus中如何編寫(xiě)sql語(yǔ)句,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11解決myBatis generator逆向生成沒(méi)有根據(jù)主鍵的select,update和delete問(wèn)題
這篇文章主要介紹了解決myBatis generator逆向生成沒(méi)有根據(jù)主鍵的select,update和delete問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09Spring中@EnableScheduling注解的工作原理詳解
這篇文章主要介紹了Spring中@EnableScheduling注解的工作原理詳解,@EnableScheduling是 Spring Framework 提供的一個(gè)注解,用于啟用Spring的定時(shí)任務(wù)(Scheduling)功能,需要的朋友可以參考下2024-01-01uploadify java實(shí)現(xiàn)多文件上傳和預(yù)覽
這篇文章主要為大家詳細(xì)介紹了java結(jié)合uploadify實(shí)現(xiàn)多文件上傳和預(yù)覽的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10Java的MyBatis框架中Mapper映射配置的使用及原理解析
Mapper用于映射SQL語(yǔ)句,可以說(shuō)是MyBatis操作數(shù)據(jù)庫(kù)的核心特性之一,這里我們來(lái)討論Java的MyBatis框架中Mapper映射配置的使用及原理解析,包括對(duì)mapper的xml配置文件的讀取流程解讀.2016-06-06將一個(gè)數(shù)組按照固定大小進(jìn)行拆分成數(shù)組的方法
下面小編就為大家?guī)?lái)一篇將一個(gè)數(shù)組按照固定大小進(jìn)行拆分成數(shù)組的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11利用Java實(shí)現(xiàn)帶GUI的氣泡詩(shī)詞特效
這篇文章主要為大家介紹了如何利用Java語(yǔ)言實(shí)現(xiàn)帶GUI的氣泡詩(shī)詞特效,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一定幫助,感興趣的可以了解一下2022-08-08