編程趣事——Java對象之死

我們都知道垃圾回收是指回收那些不再使用的對象所佔的內存區域。生動的說,在 Java 的世界裡,無用的人就要拉出去槍斃了,並且把其所佔的地盤清理,以便讓“別人“來使用。

編程趣事——Java對象之死

如何判斷對象“無用”?

關於判斷對象是否無用的算法,在JVM的發展過程中出現過兩種算法:一種是引用計數和根集算法。

引用計數算法

例如下圖中的object1的引用計數是2,GC的時候不回收,object6、object7引用計數為0,GC的時候要被回收。引用計數有個缺點:當引用產生閉環的時候即便是對象實際上已經“無用”也無法回收了,例如下圖中的 ,object4、object5、object8直接引用關係。

編程趣事——Java對象之死

根集算法

引用計數算法簡高效,早期的 Java 虛擬機中使用這個方式,但是正如上面提到的不能解決“引用閉環”的問題,後來的 Java 虛擬機中普普採用根集算法。從 GCRoot(比如一個靜態變量) 開始遍歷引用關係,能遍歷到的,叫做引用可達,遍歷不到的叫做不可達。不可達的對象就被判“死刑了”,GC的時候將被槍斃掉。

編程趣事——Java對象之死

對象回收之後的內存如何處置?

人死了、遺產處理不好會產生很多糾紛,所以有法律制度。在 JVM 的世界裡對象死了,剩下的“遺產”無非就是它佔據的那片內存空間。對象死後生下的那部分內存空間進行一下規劃的,具體算法有三種。

編程趣事——Java對象之死

標記-清除

標記就是把那些“無用的對象”標記一下,被標記的對象等於被判了死刑,也就是就可以回收了,清除就是變那些被標記了的對象清楚掉。

標記後的內存狀態:

編程趣事——Java對象之死

清除之後的內存狀態:

編程趣事——Java對象之死

我們發現,清除之後的狀態,其中的可用內存並不是連續的,也就是說內存存在碎片,如果創建一個大對象,無法分配到足夠大的連續內存空間,使得GC不得不做一次重新整理。由於可用對象和無用對象直接的內存不是連續的,所以標記的過程是要遍歷識別內存區域的,清除的過程也是要遍歷識別的,整個過程效率比較低。

標記-複製

標記的過程不變。把內存劃分為兩部分,一部分叫做預留區域(下圖虛線框中),不分配對象。在GC的時候把那些正在使用的對象複製到預留區域,然後再把非預留區域以外的內存全部清除。

標記之後內存狀態:

編程趣事——Java對象之死

複製之後內存狀態:

編程趣事——Java對象之死

清除之後內存狀態:

編程趣事——Java對象之死

解決了效率和內存碎片的問題,但是代價是昂貴的:犧牲了1/2的內存,顯然在很多情況下是無法接受的。

標記-整理

標記的過程依然不變,標記之後處於內存末端區域的正在使用的對象向前移動佔據覆蓋那些被標記了的區域(有一種碾壓的感覺),把正在使用的對象趕到一起,再把剩餘的標記對象全部清除。

標記之後內存狀態:

編程趣事——Java對象之死

移動之後的內存狀態:

編程趣事——Java對象之死

清除之後內存狀態:

編程趣事——Java對象之死

分代混合算法

在現代虛擬機(通常就是 HotSpot(TM)),使用的分代算法來處理內存,並沒有什麼新意,只是針對對象的生命週期範圍來劃分區域,不同的區域使用不同的算法。一般分為新生代和老生代,新生代由於生命不長,GC的時候大部分對象已經死亡,所以有足夠的空間作為擔保,可用使用標記-複製算法,對於老生代老生代使用標記-清除或標記-整理算法。

編程趣事——Java對象之死

Stop the world

編程趣事——Java對象之死

想象一下,你不可能在媽媽一邊打掃衛生的時候你一邊扔垃圾吧,她當然希望你乖乖做在沙發上抬起腳來別動。JVM的世界亦如此,前面我們說道使用引用關係的根集算法來標記對象是否無用,二這個引用關係只是某一時刻的“快照”,使用一個叫做OopMap的數據結構來保存的。引用關係是會隨時間變化的,所以在垃圾回收器進行垃圾回收時候就必須的有所停頓,sun把這個現象叫做“Stop the world ”。

所以頻繁的GC會影響性能,對象存活時間過長會佔用內存,在實際開發過程中我們如何去平衡內存空間和執行效率、如何去選擇對象生命週期是非常重要的。

歡迎關注筆者,筆者持續更新,私信回覆‘資料’獲取。


分享到:


相關文章: