JVM知識點掃盲系列(2)

java填坑路發表於2018-07-12

JVM的堆記憶體實現為什麼採用分代思想?

每次被小夥伴問到這種空洞的問題,簡直頭皮發麻,每次的草草解釋,感覺都是蒼白無力的語言,詞窮的我只能和他們說,演算法是慢慢優化,並演化過來的…

先來點專業的知識:1960年,McCarthy和Collins發表了第一篇有關自動動態記憶體管理(垃圾回收)的論文,而垃圾回收機制最早誕生於1958年的Lisp古老語言。垃圾回收的出現,給軟體開發帶來的眾多收益,大大消除了開發過程中的幾大類錯誤,比如嘗試對懸掛指標進行解引用,對已經釋放的記憶體進行二次釋放等。

如果沒有采用分代思想實現記憶體,會怎麼樣?

首先應該知道,新建立的物件都在堆上分配記憶體,每次垃圾回收,先標記出那些存活的物件(或者是垃圾物件),這時可以採取多種方式進行後續處理。

1、清空垃圾物件,保留存活物件。

這種方式,不會涉及物件複製,應該效率還不錯,但是存在致命的問題,可能發生GC幾次之後,就出現大量的記憶體碎片,而這些碎片已經小到不足以分配任何物件。

2、移動存活物件。

這種方式,需要把這些物件移動到記憶體的一端,可以完美解決記憶體碎片問題,但是,對於長期活躍的物件,每次GC都要進行移動,特別是對於大物件來說,這個效率實在可怕。

有一個”弱分代假說”,weak generational hypothesis,大概含義是:大多數物件都在年輕的時候死亡,而且這個假說已經在各種不同型別的程式語言中得到證實。因此可以利用這一特性,儘量的提高回收效益(回收之後所得的空間),同時減小回收時的時間開銷,這裡就需要對第二種方式進行適當的改進,對於一些長期活躍的物件,甚至大物件來說,應該儘量的減少他們被複制的次數,其實就是避免每次垃圾回收都進行復制。

所以,這裡很巧妙的引入了分代思想,把整個記憶體堆分成兩塊區域,即新生代和老年代。

上圖就是HotSpot虛擬機器的具體演算法實現了,對新生代進行了更細的劃分,分成Eden區和2個Survivor,新建立的小物件直接在Eden進行分配,大物件可以選擇在老年代進行分配(這樣可以有效的防止在YGC的時候進行移動)。

當Eden分配滿了之後,就會觸發我們熟悉的YGC進行垃圾回收,先標記存活物件,再複製到其中一個Survivor區,如果Survivor區裝不下存活物件,那說明JVM引數設定的不太合理,因為Eden區和2個Survivor的預設比例是8:1:1,但是我看到過,有同學竟然線上上環境設定了22:1:1,這個比例實在有點偏激,因為Survivor裝不下的物件,會被提前放入老年,會導致老年代提早被用完。

使用這種方式,如果一個物件經歷了多次YGC(預設是15次,但是這個值是動態變化的),依然還是存活的,我們就可以粗暴的認為這個物件已經不再年輕,可以進入老年代進行養老了,在之後的YGC,這些物件就不會再參與複製了,有效的避免了上面提到的問題。

其實採用了分代之後,會引入另外一個問題,因為YGC演算法只對新生代進行垃圾回收,但是老年代也有被用完的時候,也需要一個對應的垃圾回收演算法,很明顯,整體的演算法複雜度上升了不止一點點,而且為了能夠找出新生代全部的存活物件,還需要維護物件之間的跨代的引用,不過相對於分代回收所帶來的收益相比,這一開銷是值得的。

如何設計分代垃圾回收器,同時達到高吞吐量和低耗時,是一門長期而又精妙的藝術。

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:744677563

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導


相關文章