Java的垃圾回收機制及演算法
寫在前面:
該系列文章,主要是為了深入學習Java完成的一條鏈,推薦閱讀的整體順序為:Java的記憶體模型(根源),一個java檔案被執行的歷程,一個Java類的載入,Java的垃圾回收機制及演算法,Linux(六):系統運維常用命令 和 Java程式執行狀態的監控(實用,定位Java程式問題)
Java的垃圾回收機制
前面的已經說過關於 Java在記憶體中的記憶體劃分(Java的記憶體模型),一個java檔案執行的流程、一個Java類如何被載入,本著有始有終的原則,下面該說一下 如何 “ 死亡 ”。
Java的編譯流程已經提過了,其實更多的就是類+物件+記憶體,就是將程式設計師抽象的類檔案,載入到記憶體區,形成一個個的物件,物件可以極端的理解為就是記憶體中的一部分,記憶體劃分好塊之後,物件都在堆中建立,其中一個物件,便是堆區中的一塊記憶體。
隨著程式的執行,各種套娃的邏輯,new的使用,資料的轉存等等,都會讓堆內的物件遞增,這就涉及一個問題,堆內的記憶體是有限的,物件如何只是不停的增加,早晚會爆炸,也就是常說的堆疊溢位;最經典就是C++,習慣寫C++的小夥伴肯定知道,每次建立物件,都要想著析構,因為C++是不會自己清除無效物件的,它的記憶體空間就會不停的增加,只有研發人員自發的去釋放掉,這樣就要求寫C++的研發要時刻牢記記憶體的釋放。
但是Java就不需要,我們只需要無腦的new new new ,套娃套娃套娃,原因就是Java有一套自動的物件銷燬機制,也叫垃圾回收機制,這也是學習JVM,以及各種面試都會問到的點。
通常要聊Java的垃圾回收機制,需要搞清除三個問題:
- 哪些記憶體會被回收清理
- 怎樣回收清理
- 什麼時候會被回收清理
哪些記憶體(物件)會被回收清理
之前說過,在java中,萬物皆物件,而物件存在於堆記憶體中,要說哪些物件會被清除,其實可以理解為,哪些物件已經“死了”。也就是說哪些物件已經不再被程式需要,不再被呼叫,這裡判斷物件是否需要被回收,一般是兩種演算法:引用計數器演算法和可達性演算法
引用計數器法
原理其實很簡單,給執行的物件新增一個引用計數器,每當有一個地方引用它時,計數器+1;當引用失效時,計數器就-1,任何時刻計數器為0的物件,就視作不可能再被使用。這一種方式,實現簡單,邏輯也清晰,大部分的情況下,它都可以達到很好的效果,儘管這樣,計數器演算法還是存在但是的,但是它無法解決迴圈引用的場景,這也是主流Java虛擬機器沒有選用這一演算法的原因。
說一般存在於:虛擬機器棧、java方法區、本地方法區的物件都是可達的,也就是GCRoots物件
1、方法區靜態屬性引用的物件 全域性物件的一種,Class物件本身很難被回收,回收的條件非常苛刻,只要Class物件不被回收,靜態成員就不能被回收。
2、方法區常量池引用的物件 也屬於全域性物件,例如字串常量池,常量本身初始化後不會再改變,因此作為GC Roots也是合理的。
3、方法棧中棧幀本地變量表引用的物件 屬於執行上下文中的物件,執行緒在執行方法時,會將方法打包成一個棧幀入棧執行,方法裡用到的區域性變數會存放到棧幀的本地變量表中。只要方法還在執行,還沒出棧,就意味這本地變量表的物件還會被訪問,GC就不應該回收,所以這一類物件也可作為GC Roots。
4、JNI本地方法棧中引用的物件 和上一條本質相同,無非是一個是Java方法棧中的變數引用,一個是native方法(C、C++)方法棧中的變數引用。
5、被同步鎖持有的物件
怎樣回收清理
該演算法很簡單,使用通過可達性分析分析方法標記出垃圾,然後直接回收掉垃圾區域。簡單粗暴,即標記刪除的物件,對其進行記憶體回收;它的一個顯著問題是一段時間後,記憶體會出現大量碎片,導致雖然碎片總和很大,但無法滿足一個大物件的記憶體申請,從而導致 OOM,而過多的記憶體碎片(需要類似連結串列的資料結構維護),也會導致標記和清除的操作成本高,效率低下。
為了解決標記清除演算法的效率問題,有人提出了複製演算法。它將可用記憶體一分為二,每次只用一塊,當這一塊記憶體不夠用時,便觸發 GC,將當前存活物件複製(Copy)到另一塊上,以此往復。這種演算法高效的原因在於分配記憶體時只需要將指標後移,不需要維護連結串列等。但它最大的問題是對記憶體的浪費,使用率只有 50%。
但這種演算法在一種情況下會很高效:Java 物件的存活時間極短。據 IBM 研究,Java 物件高達 98% 是朝生夕死的,這也意味著每次 GC 可以回收大部分的記憶體,需要複製的資料量也很小,這樣它的執行效率就會很高。
在實際的Java程式中,大部分的物件存活週期都較短,基本上建立完,緊接著處理完資料就被丟棄了,大部分 Java 物件是朝生夕死的,所以我們將記憶體按照 Java 生存時間分為 新生代(Young)
和 老年代(Old)
,前者存放短命僧,後者存放長壽佛,當然長壽佛也是由短命僧升級上來的。然後針對兩者可以採用不同的回收演算法,比如對於新生代
採用複製演算法會比較高效,而對老年代
可以採用標記-清除或者標記-整理演算法。這種演算法也是最常用的。
將記憶體分代後的 GC 過程一般類似下圖所示:
-
-
-
當
Eden
區滿,觸發 Young GC,此時將Eden
中還存活的物件複製到S0
中,並清空Eden
區後繼續為新的物件分配記憶體 -
當
Eden
區再次滿後,觸發又一次的 Young GC,此時會將Eden
和S0
中存活的物件複製到S1
中,然後清空Eden
和S0
後繼續為新的物件分配記憶體 -
每經過一次 Young GC,存活下來的物件都會將自己存活次數加1,當達到一定次數後,會隨著一次 Young GC 晉升到
Old
區 -
Old
-
-
Serial GC,序列,單執行緒的收集器,執行 GC 時需要停止所有的使用者執行緒,且只有一個 GC 執行緒
-
Parallel GC,並行,多執行緒的收集器,是 Serial 的多執行緒版,執行時也需要停止所有使用者執行緒,但同時執行多個 GC 執行緒,所以效率高一些
-
什麼時候會被回收清理
-
-
Serial Old 和 Parallel Old 在
Old 區
是在 Young GC 時預測Old 區是否可以為 young 區 promote 到 old 區 的 object 分配空間,如果不可用則觸發 Old GC。這個也可以理解為是Old區
滿時。 -
CMS GC 是在
Old 區