【性能】垃圾收集器(GC)

1 概述

在Java內存運行時的各個部分中,程序計數器,虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。而Java堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。

結論:垃圾收集器主要收集的是Java堆和方法區

2 判斷對象是否存活

判斷對象是否存活,一般有兩種方法

  • 引用計數法
  • 可達性分析

2.1 引用計數法

當一個對象A被其他對象B引用時,對象A引用+1,斷開引用則-1,GC工作時,會檢查所有對象中的引用計數,如果爲0則代表要清除,>0則表示有其他對象引用不能清除。

優點:實現簡單效率高
缺點:無法解決對象之間相互循環引用問題

這種方法Java中並沒有被採用。

2.2 可達性分析

在主流的商用程序語言(Java、C#等)的主流實現中,都是通過可達性分析來判定對象是否存活的。這個算法的基本思想是:通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如圖2.1所示,object5、object6和object7雖然是相互有關聯的,但是它們到GC Roots是不可達的,所以它們將會被判定爲是可回收的對象。

在這裏插入圖片描述

圖2.1 可達性分析示例

在Java語言中,可作爲GC Roots的對象的包括以下幾類

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

即使是在可達性分析算法中不可達的對象,也並不是一定會被回收,真正被回收至少需要進行兩次標記過程:如果對象進行可達性分析後發現該對象與GC Roots之間沒有引用鏈,則會進行第一次標記然後進行一次篩選。篩選方法是該對象是否有必要執行finalize()方法。

  • 如果該對象沒有覆蓋finalize()方法,或者虛擬機已經調用過finalize()方法,則表示該對象沒有必要執行finalize()方法,此時對象會被直接回收。
  • 如果該對象覆蓋了finalize()方法,且對象未執行過finalize()方法,則將該對象放入F-Queue隊列中,由一低優先級線程執行該隊列中對象的finalize()方法。執行完畢後,GC會再次判斷該對象是否可達,如果不可達則回收,否則,對象“復活”。

3 垃圾收集算法

垃圾收集算法主要包括以下四種

  • 標記-清除算法
  • 複製算法
  • 標記-整理算法
  • 分代收集算法

3.1 標記-清除算法

標記清除算法分爲“標記”和“清除”兩個階段:首先標記所有需要被回收的對象,在標記完成後再進行統一回收所有被標記的對象。這種算法是最基礎的算法,後續的算法都是基於這種思路並針對其不足改進的算法。這種算法的不足地方有:

  • 效率,標記和清除兩個過程的效率都不高
  • 空間問題,標記-清除後會產生大量不連續的內存碎片,如果內存碎片過多,當後續程序需要分配一個比較大的內存空間時可能無法找到一塊連續的且足夠大的內存空間。

標記清除算法的執行過程如圖3.1所示:
在這裏插入圖片描述

圖3.1 標記-清除算法示意圖

3.2 複製算法

爲了解決效率問題提出了複製算法。複製算法的主要思想是:將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當其中一塊內存用完了時,就將還存活的對象複製到另一塊上面,然後再把已使用過的內存空間一次清理掉。

在這裏插入圖片描述

圖3.2 複製算法示意圖

這種算法一般用來回收新生代,其也有着缺點與不足。

  • 在對象存活率較高時,會進行較多的複製操作,降低了效率
  • 如果對象存活過多,保留區域無法全部複製存活對象,則需要額外空間進行分配擔保。

分配擔保舉例:

IBM公司的專門研究表明,新生代中的對象98%都是“朝生夕死”,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分爲一塊打的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的內存會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其它內存(這裏指老年代)進行分配擔保。

3.3 標記-整理算法

根據老年代的特點,人們提出了標記-整理算法。該算法是將還存活的對象向一邊進行移動,然後清理掉邊界以外的內存。

在這裏插入圖片描述

圖3.3 標記-整理算法示意圖

3.4 分代收集算法

該算法是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆劃分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都會發現大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對其進行分配擔保,就必須使用標記-清理或者標記-整理算法進行回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章