JVM(2)-垃圾收集器與內存分配策略 一、垃圾收集器 二、內存分配策略

一、垃圾收集器

1.如何確定對象已死

1.1.引用計數法-Reference Counting

給對象添加一個引用計數器,當有新的地方引用它時,引用計數器加1,當引用失效時,計數器減1,任意時刻計數器爲0的對象就是不可能被再使用了。這種方式實現簡單且高效,但是很難解決循環引用的問題,例如有兩個對象A、B,除了相互引用之外,並沒有可達引用可以訪問到它們中的任意一個,這種情況下其實它們已經是垃圾對象,但是它們的引用計數器都不爲零。

1.2.可達性分析-Reachability Analysis

這個方法的基本思想是通過一系列的稱爲 “GC Root”的對象作爲起點,從這些節點往下搜索,所經過的路徑稱爲引用鏈,當一個對象到“GC Root”沒有任何引用鏈相連時,則證明此對象是不可用的。這一些列GC Root對象是哪些對象呢?

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

note: 關於Java中的4中引用可參考文章《Java中的4種引用類型》

2.生存還是死亡

  1. 當一個對象與GC Root沒有任何引用鏈時,那麼它會被第一次標記並且進一步篩選,篩選的條件是此對象有沒有必要執行finalize()方法。當對象沒有覆蓋finalize()方法或者finalize()方法已經被調虛擬機調用過,虛擬機將這兩種情況視爲“沒有必要執行”。
  2. 若有必要執行,則將對象放置一個F-Queue的隊列中,並在稍後有虛擬機自動建立的、優先級低的Finalizer線程去執行,但並不承諾等待它運行結束,因爲不知道finalize()方法的的耗時,盲目等待有可能造成阻塞。
  3. finalize()方法是對象逃脫死亡命運的最後一次機會,如果在finalize()方法中成功拯救了自己,那就可以繼續存活,如果沒有逃脫,那麼它就真的被回收了。

3.方法區回收

  1. 廢棄常量回收,當一個常量沒有被任意一個引用變量引用時,此常量就是廢棄常量;

  2. 無用類卸載,無用類要滿足下面3個條件:

  • 該類的所有實例都被回收了,即Java堆中不存在該類的任何實例;
  • 加載該類的ClassLoader已經被回收;
  • 該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過發射訪問該類的方法。

4.垃圾回收算法

4.1.標記-清除(Mark-Sweep)

首先標記所有可回收的對象,在標記完成後,統一回收。
特點:標記清除這兩個過程效率都不高,另外一點就是出現內存碎片化問題;

4.2.複製算法(Coping)

將可用內存分爲大小相等的兩塊,只使用其中一塊,當這一塊用完時,將還存活的對象賦值到另外一塊中,然後在把這一塊上已使用的一次回收掉。
特點:實現簡單、高效,但這使得可用內存變爲原來的一半,代價有點兒高。
現代商業虛擬機在新生代中採用這用算法回收,將新生代按照 8:1 的比例分爲 Eden 區和兩個 Survivor 區,每次只使用Eden和其中一個Survivor,當使用完時,將Eden和Survivor上的存活對象複製到另外一個Survivor上,如果存活的對象超過Survivor的大小,則使用老年代。這樣每次使用的內存就是原來的90%,浪費的只有10%。

4.3.標記-整理(Mark-Compact)

標記過程與4.1類似,但後續過程是讓所有存活對象向一端移動,讓後直接清理掉邊界以外的內存。

4.4.分代回收算法

這並不是一種新的思想,而是根據具體的場景採用合適的回收算法。Java虛擬機一般把堆分爲新生代和老年代,新生代的特點是每次垃圾收集時都發現大批對象死去,存活率低,適合採用複製算法;而老年代存活率很高,則採用“標記-清除”或者“標記-整理”算法來進行回收。

5.HotSpot實現

5.1根節點枚舉

Stop the world,OopMap,不會爲每條指令生成對應的OopMap,只在特定的位置記錄這些信息,即安全點。

5.2.安全點

搶佔試中斷,在發生GC時,中斷所用用戶線程,如果發現某個線程不在安全點,則恢復此線程,讓其跑到安全點。現在基本沒有虛擬機這個幹。
主動式中斷,當發生GC時,只是設置標記位,讓用戶線程自己在安全點檢查這個標記,如果是ture則自己中斷掛起,另外一個檢測的地方時需要分配內存的地方。

5.3.安全區域

SafePoint似乎能很好的解決進入GC的問題,但試想這樣一個場景,當某些線程處於sleep或者blocked狀態時,虛擬機很難說等待這些線程得到CUP資源並跑到附近的安全點上,這個時間是不確定的,這就引入了安全區域的概念,當一個線程進入安全區域時,標記位自己在安全區,發生GC時,就不用管在安全區域的線程了,當要離開安全區時它檢查是否已經完成了GC,否則中斷掛起等待。

6.垃圾收集器

圖中的連線表示垃圾收集器可以配合工作

6.1.Serial

新生代收集器,單線程GC,採用複製算法,所用用戶線程跑到SafePoint中斷,然後執行GC,GC完恢復用戶線程,這會導致Stop The World。

6.2.ParNew

多線程版的 Serial。

6.3.Parallel Scavenge

新生代收集器,複製算法,並行多線程收集器,看上去和ParNew一樣,但它的關注點不一樣,前兩個收集器的關注點是儘量縮短GC導致用戶線程的停頓時間,而這個收集器的關注點是吞吐量,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。

6.4.Serial Old

老年代收集器,單線程,使用“標記-整理”算法。

6.5 Parallel Old

是Parallel Scavenge的老年代版本,使用“標記-整理”算法。

6.7 CMS

Concurrent Mark Sweep收集器,關注點是獲得最短的回收停頓時間,從名字就看得出來是“標記-清除”算法,它運作過程分爲四個步驟:

  • 初始標記(CMS initial mark)- Stop The World
  • 併發標記(CMS concurrent mark)
  • 重新標記(CMS remark)- Stop The World
  • 併發清除(CMS concurrent sweep)

在四個步驟中,其中併發標記、併發清除時間是相對較長的,都是可以和用戶線程併發執行的,所以Stop The World時間是很短的,總體上來看就是併發執行的,這對要求響應速度較快的應用場景比較適合。CMS還遠達不到完美,它有一下幾個缺點:

  • 對CUP資源敏感,搶佔CUP資源將導致用戶線程的CUP資源減少而變得緩慢;
  • 無法處理浮動垃圾,在併發回收垃圾時,用戶線程會產生新的垃圾對象,這些垃圾要等下次回收;由於在併發回收的過程用戶線程還在工作,這就需要預留一定的內存空間給用戶線程,導致內存空間利用率下降;
  • CMS採用的是標記-清除算法,這就導致內存碎片化。若出現內存空間還很多,但由於碎片化的情況,無法滿足大對象的分配,當頂不住要觸發Full GC時開啓內存碎片合併整理過程,這個過程是不能併發的,會Stop The World。

6.8.G1

Garbage First,將內存分爲多個Region,使用Remembered Set 避免全盤掃描,標記-整理算法。

  • 初始標記(initial Marking)
  • 併發標記(Concurrent Marking)
  • 最終標記(Final Marking)
  • 篩選回收(Live Data Counting Evacuation)

優點:

  • 並行與併發,能充分利用CUP資源;
  • 空間整理,與CMS的標記-清理相比,它採用的是標記-整理,從局部Region來看又是複製算法;
  • 可預測停頓,可以讓使用這指定在長度爲M毫秒的是時間內,垃圾收集時間不能超過N毫秒;

二、內存分配策略

  1. 對象優先新生代Eden區分配
    大多數情況下,對象在新生代Eden區分配,若Eden區無法分配,則虛擬機會觸發一次Minor GC。

Minor GC-新生代; Major GC/Full GC-老年代

  1. 大對象直接直接進入老年代,大對象的界定可通過參數設定;
  2. 長期存活的對象進入老年代,對象的年齡,Minor GC一次且能被Survivor分區容納則加1,默認是15歲進入老年代;
  3. 動態年齡判斷,虛擬機並非永遠要求對象的年齡到達了MaxTenuringThreshold才能晉升老年代,如果相同年齡的對象總時佔據Survivor分區的一半及以上,年齡大於或者等於改年齡的對象就可以直接進入老年代,無須等到年齡到達MaxTenuringThreshold;
  4. 空間分配擔保,處理Minor GC的風險問題,老年代爲新生代擔保,根據具體情況看是是否執行Full GC,比如老年代的剩餘連續空間比新生代大,那就沒有必要Full GC,這種情況下Minor GC是沒有風險的。

https://blogs.oracle.com/jonthecollector/our-collectors

上一篇:JVM(1)-運行時數據區
下一篇:JVM(3)-類加載機制

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