GC回收算法--當女友跟你提分手!


Java語言引入了垃圾回收機制,讓C++語言中令人頭疼的內存管理問題迎刃而解,使得我們Java狗每天開開心心地創建對象而不用管對象死活,這些都是Java的垃圾回收機制帶來的好處。但是Java的垃圾回收機制的核心原理是什么呢?今天我們來聊聊GC回收算法吧。

JVM的GC回收場景很復雜,不是單個算法就可以搞定的,大致可以分為可達性分析算法、標記-清除算法、標記-整理算法、分代回收算法、復制算法

 

廣場上,女朋友突然跟你鬧分手,然后頭也不回地一個人走了,留下你一個人站在樹下,BGM緩緩響起“雪花飄飄 北風嘯嘯 天地 一片 蒼茫~~~”樹葉紛紛落下,這時的你仿佛被夏洛特里的元華附身,成了全世界最悲傷的人。當你沉浸在悲傷不可自拔,旁邊的環衛大媽一臉嫌棄看着你“年輕人你挪一下,別擋到我掃地”。

對你沒猜錯,地上的落葉,就是GC垃圾回收算法的核心--可達性分析算法

 可達性分析算法

輕風乍起,泛黃的樹葉紛紛掉下,剛分手的你不禁長嘆“葉子的離開是風的追求還是樹的不挽留”,當葉子從枝頭掉落的那一刻,它跟樹就再也沒有任何關系。同樣的,可達性分析算法的基本思路就是JVM內存中的對象以樹的形式管理,我們稱之為"GC tree"。GC tree的根節點叫做GC Roots,通過一些列GC Roots為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象不可用了。

圖中的對象1、2、3、4不管哪個都可以找到與GC Roots相連的引用鏈,屬於存活對象,而對象5、6、7雖然彼此相互有聯系,但是他們到GC Roots是不可達的,所以屬於死亡對象。

有哪些對象可以作為GC Roots呢?

虛擬機棧(棧楨中的本地變量表)中的引用的對象
方法區中的類靜態屬性引用的對象
方法區中的常量引用的對象
本地方法棧中JNI(Native方法)的引用的對象

標記-清除算法(Mark-Sweep) 分手不是馬上分

二次標記

當女朋友跟你鬧分手,她是真的要跟你分嗎?太天真了少年!!!女人都是感性動物,刀子嘴豆腐心,她只是給你判了個死緩,如果你什么都不做,那我沒話說,注孤生吧小伙子;如果你態度端正,那你還有得救!

同樣的,當GC線程遍歷GC tree檢測到無用對象的時候,並不是立馬人道毀滅,只是先給它做個標記,告訴對象你已經上了槍斃名單。這里是第一次標記

挽留愛情該如何做?說情話哄她,拉她去心心念念的館子吃頓好的,又或者去商場給她買向往已久的迪奧999口紅······這些套路我就不說了,反正只要能讓女朋友開心,什么付出都是值得的。

當對象第一次被標記的時候,GC線程會去檢查此對象是否有必要執行finalize()方法。finalize()方法還記得吧?finalize()定義:finalize()是Object的protected方法,子類可以覆蓋該方法以實現資源清理工作,GC在回收對象之前調用該方法。什么意思?就是意味着如果我們重寫該方法的話,那么在GC回收之前該方法會被執行。

但是有兩種情況GC線程會認為沒有必要去執行。

1.對象沒有覆寫finalize()的。女友跟你鬧分手,你卻像沒事人一樣回家繼續擼游戲,小伙子你心可真大。

2.finalize()已經被虛擬機執行過的。女友此時內心OS:上次吵架你送我一只迪奧999賠罪,這次你又送,你就不知道我這段時間一直想買蘿卜丁嗎?一定是在敷衍我!呵!男人!

生存還是死亡 要愛情還是要自由 That is a question!

如果對象被判定需要執行finalize()方法,那么它將會放置在一個叫F-Queue的隊列里面挨個等待執行,對象自我救贖的機會來了!如果對象想在finalize()中成功拯救自己,只要重新與GC Roots建立關聯即可,比如把自己賦值給某個類對象或者對象的成員變量,那么在第二次標記的時候它從“即將回收”的集合中中被移除;如果這時它還沒有建立關聯,那么它這次真的是GG了,我們用一首《涼涼》給它送別吧。

圖解:

以上就是標記-清除算法,不過它有兩點不足之處

1.效率問題,標記和清除過程的效率不高。

2.空間問題,標記清除后會產生大量不連續的內存碎片,碎片太多可能會導致以后在程序中需要分配占用較大連續空間的對象(如數組)時,無法找到足夠的連續內存而不得不提前觸發下另一次垃圾收集動作。

為了解決這些問題,“復制算法”應運而生。

復制算法(Copying) 物種大逃亡 諾亞方舟!

 復制算法思路比較簡單:將內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當一塊內存空間滿了,就將還活着的對象復制到另外一塊,然后再將之前那塊內存空間徹底清空。有點像《聖經》里的一個故事:大洪水要來了,生物紛紛逃上諾亞方舟以躲避災難。這樣玩的話每次都是對整個半區進行內存回收,內存分配的時候也不用考慮內存碎片的情況,簡單粗暴讓人喜歡!只不過這種算法將內存縮小為了原來的一半,代價太高昂了,我們要知道,內存是很寶貴的資源!

黃金比例 8:1:1

醫學研究證明,感冒是由病毒引起的。咳咳開個玩笑!軟件團隊研究表明,內存中的絕大部分對象都是“朝生暮死”的,所以完全沒必要非要按照1:1的比例來玩。而是把內存分成了一塊較大的Eden區(伊甸區)和兩塊較小的Survivor區(幸存區),每次都使用伊甸區和其中一塊幸存區(我們取個別名叫幸存者1號吧,另外一塊取名幸存者2號)。當回收的時候,將伊甸區和幸存者1號區域里面的對象一次性復制到幸存者2號里面,最后對伊甸區和幸存者1號進行清算,里面的所有對象不管生存還是死亡徹底清除干凈,比滅霸打個響指還厲害!

圖解:

 

現在HotSpot虛擬機默認伊甸區和兩塊幸存區的比例大小為8:1:1,這樣平時工作的時候,只有10%的內存會被浪費掉,這樣是不是很划算呢?

看到這里有人鞋會問,幸存區為什么要分為兩塊?比例9:1才是最完美吧?NO!NO!NO!,這里不得不引出另一個算法--分代收集算法了。

分代收集算法(Generational Collection) 歷經考驗 修成正果

有沒有發現,所謂的愛情其實都是要經歷無數次的磨合,無數次的考驗,在一起的兩個人只有經受住了這些磨難,才會走進婚姻的殿堂,有了圓滿的結局,而經受不住這些考驗,那雙方就只有各自安好,相忘江湖。

在我們每次GC回收的時候,都會有一小部分對象活下來,然后一直活到下一次GC再次被檢測。最近很火的吃雞游戲,玩家不管用什么手段,剛槍也好苟也好,只求活下去成為最后的幸存者。而Java對象也是這樣,經歷GC的層層考驗,最終成了打不死的小強。這時候該輪到GC不爽了,你丫的每次都浪費我的時間,小強內心OS"就喜歡看你不爽我又干不掉我的樣子!",對於這批頑固分子,GC作為執法者決定眼不見為凈,於是委托JVM專門划分出一塊區域給他們頤養天年,從此天涯是路人。而划分出的這塊區域就是赫赫有名的“老年代”了,而與之相對應的就是之前GC頻繁的“新生代”。

一個對象該如何從“新生代”跑到“老年代”去呢?

我們創建一個對象,它的對象頭里面會有一個GC分代標識,每經歷一次GC如果能活下來該標識+1,當加到一定次數后,GC會判定該對象是個老流氓,於是乎把它從“新生代”轉移到“老年代”了,安排!具體參考我的另一篇博客《假如Java對象是個人······》

新生代每經歷一次GC,幸存者2號區域活下來的對象年齡標識自動+1,然后判斷是否滿15歲(默認值15次),如果滿15歲了,那么就從幸存者2號復制到“老年代”里面取頤養天年,如果沒有的話,那么就復制到幸存者1號區域里面去,然后幸存者2號區域被清空。

由於老年代對象存活率極高,用不着復制算法這一套。於是有人提出了另外一種算法叫做“標記-整理算法”

標記-整理算法(Mark-Compact)

標記-整理算法其實基本過程跟“標記-刪除”算法差不多,只不過后續的步驟不是對無用對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理到端邊界以外的內存。這樣就完美解決了“標記-清除算法”內存碎片化的問題。

圖解:

講到這里,JAVA的GC回收算法基本就差不多了。我們的GC就是針對內存中的不同區域,采取合理的算法從而達到自動清理的效果。新生代的對象大多數朝生暮死,就采用“復制算法”,老年代的對象存活率極高,就采用“標記-刪除算法”或者“標記-整理算法”。

現在是不是覺得GC回收算法沒有想象中那么神秘?希望我的理解能給你帶來一點幫助,由於人懶,圖片都是網上直接拿來用的。另外,如果現實中跟女友有摩擦,該服軟還是得服軟,男人就應該表現得大度一點,畢竟兩個人相處不易,更何況她還是將和你共度余生的人,不要因為一時沖動而抱憾終生。額······我仿佛又聞到了愛情的酸臭味!

 

 參考資料:《深入理解Java虛擬機——JVM高級特性與最佳實踐(第2版)》


免責聲明!

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



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