寫在前面:
該系列文章,主要是為了深入學習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 區