JVM——垃圾收集器與記憶體分配

it_was發表於2020-09-20

:fast_forward:本章講述的是JVM的記憶體分配和回收
首先我們清楚,Java的記憶體區域主要有五個:程式計數器,虛擬機器棧,本地方法棧,堆和方法區。然而前三個都是執行緒私有的,隨著執行緒的建立而建立,消亡而消亡,棧中每一個棧幀分配的大小也是確定的(類載入完成後也就確定),即方法結束或者執行緒消亡的時候記憶體也就跟著回收了,不必擔心太多。
但是,堆和方法區的分配卻具有明顯的不確定性,因為你必須在程式執行期間才能知道具體分配多少記憶體,所以Java的垃圾收集器關注的就是堆和方法區!

ok,既然要做垃圾回收,那麼其物件就是那些已經死亡的物件,ok那我們如何判斷物件死亡?

:boom: 2.1引用計數法
很簡單,就是給每個物件增加一個引用計數器,如果其他地方有引用這個物件,那麼就將其計數器加一;引用失效的時候就減一;任何時候如果垃圾收集器發現某物件的引用計數器為0,就代表這個物件已經死亡,可以回收!
:+1:優點:簡單高效
:-1:缺點:計數器佔用了一些額外的記憶體,重點是需要大量的額外處理才能保證正確的工作,譬如說迴圈引用問題!

既然談到了引用我們不妨說下Java中的引用概念,主要有四種:

  • 強引用,普遍存在的,例如最經常的 “Object obj = new Object();”程式碼,即只要強引用關係還在,垃圾回收器就永遠不會回收這些被引用的物件
  • 軟引用,描述一些還有用但非必須的物件,在發生oom之前,會對這些軟引用的物件進行二次回收,如果還不行只好丟擲oom
  • 弱引用,強度更弱, 但凡讓垃圾回收器碰到必定回收!
  • 虛引用,最弱了,根本不影響物件的生存情況,唯一用來將這個物件回收時會受到一個系統通知

:boom:2.2可達性分析法
可達性分析演算法是目前主流的判斷物件死亡的演算法,此演算法主要是以一系列稱為“GC Roots”的跟物件作為起點,向下遍歷搜尋走過的路徑,這些路徑被稱為引用鏈!如果某個物件到GC Roots沒有任何引用鏈相連,那麼稱這個物件不可達!即判定其為死亡!
:exclamation:GC Roots

  • 虛擬機器棧中引用的物件(棧幀中的本地變數表)
  • 本地方法棧中JNT(即常說的native方法)引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 所有被同步鎖持有的物件

:warning:難道物件在可達性分析演算法中不可達,就說明其一定必須死亡嗎?
不一定!不可達物件還處於“緩刑”階段,即要真正判斷一個物件死亡,至少要經歷兩次標記過程!
:clock1:第一次標記:當物件不可達時進行一次標記
:clock2:篩選:從這些標記中進行篩選,檢查是否有必要執行finalize方法!如果重寫了並且沒有被呼叫過,ok,將放置在一個F-queue佇列,然後由虛擬機器的一條finalizer低優先順序執行緒去執行它,而且還不一定執行成功。否則就是不必要執行
:clock4:第二次標記:從F-queue佇列中檢查,如果物件成功自救,ok你活了下來,否則真正回收

垃圾收集器的工作區域大部分都集中在堆,對於方法區的回收也有一定的規範,只不過價效比不高,但還是要強調一下
收集的物件是:廢棄的常量和不再使用的型別!!
:boom:在大量使用反射,動態代理等位元組碼的框架,通常都需要Java虛擬機器具備型別解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力!

5.1 分代收集理論

  • 弱分代假說
  • 強分代假說
  • 跨代引用假說

5.2 標記-清除演算法

:thumbsup:簡單易實現
:thumbsdown:

  • 執行效率不穩定,如果存在大量被回收物件則需要進行長時間的標記
  • 空間利用率低下,因為標記清除這種演算法會產生大量的記憶體碎片,導致之後如果有較大物件在新生代分配不到記憶體而提前觸發一次垃圾收集

5.3 標記-整理演算法

:thumbsup::空間利用率高,不會產生記憶體碎片
:thumbsdown::移動負擔較重,需要更新存活物件引用,甚至或造成長時間使用者執行緒的”Stop The World”但很短,最關鍵的是低停頓!

5.4 複製演算法

:thumbsup::實現簡單,執行高效
:thumbsdown::浪費空間,畢竟另一半survivor區是不可用的。

IBM公司之前做過研究,提出新生代中有98%的物件都熬不過第一輪收集的,所以沒必要將新生代的記憶體空間劃分為一比一。現在HotSpot虛擬機器大都也不會採取這種劃分,更多的是將eden區和survivor區劃分為8:1

話不多說先上一副圖,如果兩個收集器之間有連線,說明他們之間可以搭配使用
JVM——垃圾收集器與記憶體分配

6.1 Serial收集器

最基礎的一款垃圾收集器,是一款單執行緒的垃圾收集器,他強調在垃圾收集的過程中,必須暫停其他執行緒的工作,直到收集結束。

JVM——垃圾收集器與記憶體分配
—————————————Serial/Serial Old————————-
優點:簡單而高效(單執行緒嘛,沒有執行緒互動的開銷),消耗記憶體少
缺點:”Stop The World”時間較長,使用者體驗差!

雖然使用者體驗惡劣,早期hotspot虛擬機器設計者表示十分委屈:“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實的坐在凳子上,如果她一邊打掃,你一邊扔紙屑,什麼時候能打掃完!!!”

6.2 ParNew收集器

其實就是Serial收集器的多執行緒並行版本,在單核處理器下並不比Serial收集器有多大優勢,但是在多核環境下有著明顯的優勢

JVM——垃圾收集器與記憶體分配
—————————————ParNew/Serial Old——————————–

6.3 Parallel Scavenge收集器

和ParNew非常相似,但是關注點不同,ParNew和CMS收集器重點關注如何縮短垃圾收集時使用者執行緒的停頓時間,而這款主要關注吞吐量!

                使用者執行程式碼的時間
吞吐量 =  ——————————————————————————————————
             使用者執行程式碼的時間 + 垃圾收集的時間

6.4 CMS收集器

JVM——垃圾收集器與記憶體分配
CMS收集器是一款以獲取最短停頓時間的垃圾收集器,主要包括四個過程

  1. 初始標記 : 很快
  2. 併發標記 :與使用者執行緒併發執行
  3. 重新標記 :修正併發標記期間使用者執行緒的不一致性
  4. 併發清除 :與使用者執行緒併發執行

優點:第一個真正意義上的併發收集器,雖然也需要”Stop The World”但很短,最關鍵的是低停頓!!!
缺點:

  • CMS對處理器資源十分敏感
  • 無法處理浮動垃圾,有可能出現失敗而導致一次完全的Full GC出現!
  • 記憶體碎片嚴重,會給記憶體分配帶來巨大壓力!

6.5 Garbage First收集器

JVM——垃圾收集器與記憶體分配

同樣包括四個過程:

  1. 初始標記(Initial Marking)
  2. 併發標記(Concurrent Marking)
  3. 最終標記(Final Marking)
  4. 篩選回收(Live Data Counting and Evacuation)

:boom: G1收集器的優勢:

  • 並行與併發

  • 分代收集

  • 空間整理 (標記整理演算法,複製演算法)

  • 可預測的停頓(G1處理追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒)

使用G1收集器時,Java堆的記憶體佈局是整個規劃為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。
G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在真個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲取的空間大小以及回收所需要的時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的又來)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘量可能高的回收效率

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章