JVM垃圾收集算法與垃圾收集器

一.如何判斷對象是否存活

GC動作發生之前,需要確定堆內存中哪些對象是存活的,一般有兩種方法:引用計數法和可達性分析法。
1、引用計數法
在對象上添加一個引用計數器,每當有一個對象引用它時,計數器加1,當使用完該對象時,計數器減1,計數器值爲0的對象表示不可能再被使用。
引用計數法實現簡單,判定高效,但不能解決對象之間相互引用的問題。
2、可達性分析法
通過一系列稱爲 “GC Roots” 的對象作爲起點,從這些節點開始向下搜索,搜索路徑稱爲 “引用鏈”,以下對象可作爲GC Roots:
本地變量表中引用的對象
方法區中靜態變量引用的對象
方法區中常量引用的對象
Native方法引用的對象
當一個對象到 GC Roots 沒有任何引用鏈時,意味着該對象可以被回收。

在可達性分析法中,判定一個對象objA是否可回收,至少要經歷兩次標記過程:
1、如果對象objA到 GC Roots沒有引用鏈,則進行第一次標記。
2、如果對象objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue隊列中,由一個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalize()方法。finalize()方法是對象逃脫死亡的最後機會,GC會對隊列中的對象進行第二次標記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯繫,那麼在第二次標記時,objA會被移出“即將回收”集合。

二.java GC是在什麼時候,對什麼東西,做了什麼事情?

在什麼時候:
1.新生代有一個Eden區和兩個survivor區,首先將對象放入Eden區,如果空間不足就向其中的一個survivor區上放,如果仍然放不下就會引發一次發生在新生代的minor GC,將存活的對象放入另一個survivor區中,然後清空Eden和之前的那個survivor區的內存。在某次GC過程中,如果發現仍然又放不下的對象,就將這些對象放入老年代內存裏去。
2.大對象以及長期存活的對象直接進入老年區。
3.當每次執行minor GC的時候應該對要晉升到老年代的對象進行分析,如果這些馬上要到老年區的老年對象的大小超過了老年區的剩餘大小,那麼執行一次Full GC以儘可能地獲得老年區的空間。
對什麼東西:從GC Roots搜索不到,而且經過一次標記清理之後仍沒有復活的對象。
做什麼:
新生代:複製清理;
老年代:標記-清除和標記-壓縮算法;
永久代:存放Java中的類和加載類的類加載器本身。

三.垃圾收集算法

1、標記-清除算法
對待回收的對象進行標記。
算法缺點:效率問題,標記和清除過程效率都很低;空間問題,收集之後會產生大量的內存碎片,不利於大對象的分配。
2、複製算法
複製算法將可用內存劃分成大小相等的兩塊A和B,每次只使用其中一塊,當A的內存用完了,就把存活的對象複製到B,並清空A的內存,不僅提高了標記的效率,因爲只需要標記存活的對象,同時也避免了內存碎片的問題,代價是可用內存縮小爲原來的一半。
3、標記-整理算法
在老年代中,對象存活率較高,複製算法的效率很低。在標記-整理算法中,標記出所有存活的對象,並移動到一端,然後直接清理邊界以外的內存。
對象標記過程
在可達性分析過程中,爲了準確找出與GC Roots相關聯的對象,必須要求整個執行引擎看起來像是被凍結在某個時間點上,即暫停所有運行中的線程,不可以出現對象的引用關係還在不斷變化的情況。
如何快速枚舉GC Roots?
GC Roots主要在全局性的引用(常量或類靜態屬性)與執行上下文(本地變量表中的引用)中,很多應用僅僅方法區就上百兆,如果進行遍歷查找,效率會非常低下。
在HotSpot中,使用一組稱爲OopMap的數據結構進行實現。類加載完成時,HotSpot把對象內什麼偏移量上是什麼類型的數據計算出來存儲到OopMap中,通過JIT編譯出來的本地代碼,也會記錄下棧和寄存器中哪些位置是引用。GC發生時,通過掃描OopMap的數據就可以快速標識出存活的對象。
如何安全的GC?
線程運行時,只有在到達安全點(Safe Point)才能停頓下來進行GC。基於OopMap數據結構,HotSpot可以快速完成GC Roots的遍歷,不過HotSpot並不會爲每條指令都生成對應的OopMap,只會在Safe Point處記錄這些信息。所以Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致運行時的性能問題。大部分指令的執行時間都非常短暫,通常會選擇一些執行時間較長的指令作爲Safe Point,如方法調用、循環跳轉和異常跳轉等。
發生GC時,如何讓所有線程跑到最近的Safe Point再暫停?
當發生GC時,不直接對線程進行中斷操作,而是簡單的設置一箇中斷標誌,每個線程運行到Safe Point的時候,主動去輪詢這個中斷標誌,如果中斷標誌爲真,則將自己進行中斷掛起。這裏忽略了一個問題,當發生GC時,運行中的線程可以跑到Safe Point後進行掛起,而那些處於Sleep或Blocked狀態的線程在此時無法響應JVM的中斷請求,無法到Safe Point處進行掛起,針對這種情況,可以使用安全區域(Safe Region)進行解決。
Safe Region是指在一段代碼片段中,對象的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。
1、當線程運行到Safe Region的代碼時,首先標識已經進入了Safe Region,如果這段時間內發生GC,JVM會忽略標識爲Safe Region狀態的線程;
2、當線程即將離開Safe Region時,會檢查JVM是否已經完成GC,如果完成了,則繼續運行,否則線程必須等待直到收到可以安全離開Safe Region的信號爲止;

四.垃圾收集器

這裏寫圖片描述
上圖展示了7種不同分代的收集器,如果兩兩之間存在連線,說明可以組合使用。
1、Serial收集器(串行GC)
Serial 是一個採用單個線程並基於複製算法工作在新生代的收集器,進行垃圾收集時,必須暫停其他所有的工作線程。對於單CPU環境來說,Serial由於沒有線程交互的開銷,可以很高效的進行垃圾收集動作,是Client模式下新生代默認的收集器。
2、ParNew收集器(並行GC)
ParNew其實是serial的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲與Serial一樣。
3、Parallel Scavenge收集器(並行回收GC)
Parallel Scavenge是一個採用多線程基於複製算法並工作在新生代的收集器,其關注點在於達到一個可控的吞吐量,經常被稱爲“吞吐量優先”的收集器。
4、Serial Old收集器(串行GC)
Serial Old 是一個採用單線程基於標記-整理算法並工作在老年代的收集器,是Client模式下老年代默認的收集器。
5、Parallel Old收集器(並行GC)
Parallel Old是一個採用多線程基於標記-整理算法並工作在老年代的收集器。在注重吞吐量以及CPU資源敏感的場合,可以優先考慮Parallel Scavenge和Parallel Old的收集器組合。
6、CMS收集器(併發GC)
CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間爲目標的收集器,工作在老年代,基於“標記-清除”算法實現,整個過程分爲以下4步:
1、初始標記:這個過程只是標記以下GC Roots能夠直接關聯的對象,但是仍然會Stop The World;
2、併發標記:進行GC Roots Tracing的過程,可以和用戶線程一起工作。
3、重新標記:用於修正併發標記期間由於用戶程序繼續運行而導致標記產生變動的那部分記錄,這個過程會暫停所有線程,但其停頓時間遠比並發標記的時間短;
4、併發清理:可以和用戶線程一起工作。
CMS收集器的缺點:
1、對CPU資源比較敏感,在併發階段,雖然不會導致用戶線程停頓,但是會佔用一部分線程資源,降低系統的總吞吐量。
2、無法處理浮動垃圾,在併發清理階段,用戶線程的運行依然會產生新的垃圾對象,這部分垃圾只能在下一次GC時收集。
3、CMS是基於標記-清除算法實現的,意味着收集結束後會造成大量的內存碎片,可能導致出現老年代剩餘空間很大,卻無法找到足夠大的連續空間分配當前對象,不得不提前觸發一次Full GC。
7、G1收集器
G1(Garbage First)是JDK1.7提供的一個工作在新生代和老年代的收集器,基於“標記-整理”算法實現,在收集結束後可以避免內存碎片問題。
G1優點:
1、並行與併發:充分利用多CPU來縮短Stop The World的停頓時間;
2、分代收集:不需要其他收集配合就可以管理整個Java堆,採用不同的方式處理新建的對象、已經存活一段時間和經歷過多次GC的對象獲取更好的收集效果;
3、空間整合:與CMS的”標記-清除”算法不同,G1在運行期間不會產生內存空間碎片,有利於應用的長時間運行,且分配大對象時,不會導致由於無法申請到足夠大的連續內存而提前觸發一次Full GC;
4、停頓預測:G1中可以建立可預測的停頓時間模型,能讓使用者明確指定在M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

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