必知必會JVM三-面試必備,JVM堆記憶體詳解

語言: CN / TW / HK

一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第19天,[點選檢視活動詳情]


上一節我們瞭解到JVM的執行時資料區的5個模組,今天重點講一下JVM的堆記憶體模型。這些都是大廠面試必備的哦,同學們請注意聽講

一、堆的記憶體區域

1.1 堆記憶體區域介紹

在jvm的堆記憶體中有三個區域:

  1. 年輕代:用於存放新產生的物件。
  2. 老年代:用於存放被長期引用的物件。
  3. 持久帶:用於存放Class,method元資訊(1.8之後改為元空間)。

年輕代\ 年輕代中包含兩個區:Eden 和survivor,並且用於儲存新產生的物件,其中有兩個survivor區

老年代\ 年輕代在垃圾回收多次都沒有被GC回收的時候就會被放到老年代,以及一些大的物件(比如快取,這裡的快取是弱引用),這些大物件可以不進入年輕代就直接進入老年代

持久代\ 持久代用來儲存class,method元資訊,大小配置和專案規模,類和方法的數量有關。

元空間

JDK1.8之後,取消perm永久代,轉而用元空間代替

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。並且可以動態擴容。那麼使用元空間會有哪些問題呢?同學們可以思考下。

1.2 為什麼分代?

因為不同物件的生命週期是不一樣的。80%-98%的物件都是“朝生夕死”,生命週期很短,大部分新物件都在年輕代,可以很高效地進行回收,不用遍歷所有物件。而老年代物件生命週期一般很長,每次可能只回收一小部分記憶體,回收效率很低。

年輕代和老年代的記憶體回收演算法完全不同,因為年輕代存活的物件很少,標記清楚再壓縮的效率很低,所以採用複製演算法將存活物件移到survivor區,更高效。而老年代則相反,存活物件的變動很少,所以採用標記清楚壓縮演算法更合適。

1.3 記憶體分配策略

1.3.1、 優先在Eden區分配

在大多數情況下, 物件在新生代Eden區中分配, 當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將Eden區和其中一塊Survivor區內尚存活的物件放入另一塊Survivor區域, 如果在Minor GC期間發現新生代存活物件無法放入空閒的Survivor區, 則會通過空間分配擔保機制使物件提前進入老年代(空間分配擔保見下).

1.3.2、大物件直接進入老年代

Serial和ParNew兩款收集器提供了-XX:PretenureSizeThreshold的引數, 令大於該值的大物件直接在老年代分配, 這樣做的目的是避免在Eden區和Survivor區之間產生大量的記憶體複製(大物件一般指 需要大量連續記憶體的Java物件, 如很長的字串和陣列), 因此大物件容易導致還有不少空閒記憶體就提前觸發GC以獲取足夠的連續空間.

1.3. 3、長期存活物件進入老年區

如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將物件年齡設為1,物件在Survivor區中每熬過一次 Minor GC,年齡就增加1,當它的年齡增加到一定程度(預設為15)_時,就會被晉升到老年代中。

1.3. 4、物件年齡動態判定

如果在 Survivor空間中相同年齡所有物件大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代

1.3. 5、空間分配擔保

在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管這次Minor GC是有風險的,如果擔保失敗則會進行一次Full GC;如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。

HotSpot預設是開啟空間分配擔保的。

二、GC執行的機制

由於物件進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種型別:Minor GC和Full GC。

2.1 Minor GC(young GC)

一般情況下,當新物件生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出來。

2.2 Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個堆進行回收,所以比Minor GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

1.年老代(Tenured)被寫滿

2.持久代(Perm)被寫滿

3.System.gc()被顯示呼叫

4.上一次GC之後Heap的各域分配策略動態變化

2.3 物件生死判定方法

那我們瞭解JVM的GC機制之後,那滿足什麼條件的物件才會被GC掉呢?

1、引用計數:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決物件相互迴圈引用的問題。

2、可達性分析演算法

在主流商用語言(如Java、C#)的主流實現中, 都是通過可達性分析演算法來判定物件是否存活的: 通過一系列的稱為 GC Roots 的物件作為起點, 然後向下搜尋; 搜尋所走過的路徑稱為引用鏈/Reference Chain, 當一個物件到 GC Roots 沒有任何引用鏈相連時, 即該物件不可達, 也就說明此物件是不可用的, 如下圖: Object5、6、7 雖然互有關聯, 但它們到GC Roots是不可達的, 因此也會被判定為可回收的物件:

在Java, 可作為GC Roots的物件包括:

  1. 方法區: 類靜態屬性引用的物件;
  2. 方法區: 常量引用的物件;
  3. 虛擬機器棧(本地變量表)中引用的物件.
  4. 本地方法棧JNI(Native方法)中引用的物件。

注: 即使在可達性分析演算法中不可達的物件, VM也並不是馬上對其回收, 因為要真正宣告一個物件死亡, 至少要經歷兩次標記過程: 第一次是在可達性分析後發現沒有與GC Roots相連線的引用鏈, 第二次是GC對在F-Queue執行佇列中的物件進行的小規模標記(物件需要覆蓋finalize()方法且沒被呼叫過).

三、GC原理-垃圾回收演算法

Java與C++等語言最大的技術區別:自動化的垃圾回收機制(GC),那麼為什麼要了解GC和記憶體分配策略呢?

  • 面試需要
  • GC對應用的效能是有影響的;
  • 寫程式碼有好處

棧:棧中的生命週期是跟隨執行緒,所以一般不需要關注

堆:堆中的物件是垃圾回收的重點

方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點

目前為止,jvm已經發展處四種比較成熟的垃圾收集演算法:

  1. 標記-清除演算法;
  2. 複製演算法;
  3. 標記-整理演算法;
  4. 分代收集演算法

3.1 標記-清除演算法

這種垃圾回收一次回收分為兩個階段:標記、清除。首先標記所有需要回收的物件,在標記完成後回收所有被標記的物件。這種回收演算法會產生大量不連續的記憶體碎片,當要頻繁分配一個大物件時,jvm在新生代中找不到足夠大的連續的記憶體塊,會導致jvm頻繁進行記憶體回收(目前有機制,對大物件,直接分配到老年代中)

優點

  • 利用率百分之百

缺點

  • 標記和清除的效率都不高(比對複製演算法)
  • 會產生大量的不連續的記憶體碎片

3.2 複製演算法

這種演算法會將記憶體劃分為兩個相等的塊,每次只使用其中一塊。當這塊記憶體不夠使用時,就將還存活的物件複製到另一塊記憶體中,然後把這塊記憶體一次清理掉。這樣做的效率比較高,也避免了記憶體碎片。但是這樣記憶體的可使用空間減半,是個不小的損失。

優點

  • 簡單高效,不會出現記憶體碎片問題

缺點

  • 記憶體利用率低,只有一半
  • 存活物件較多時效率明顯會降低

3.3 標記-整理演算法

這是標記-清除演算法的升級版。在完成標記階段後,不是直接對可回收物件進行清理,而是讓存活物件向著一端移動,然後清理掉邊界以外的記憶體

優點

  • 利用率百分之百
  • 沒有記憶體碎片

缺點

  • 標記和清除的效率都不高
  • 效率相對標記-清除要低

3.4.分代收集演算法

當前商業虛擬機器都採用這種演算法。首先根據物件存活週期的不同將記憶體分為幾塊即新生代、老年代,然後根據不同年代的特點,採用不同的收集演算法

新生代: 每次垃圾收集都能發現大批物件已死, 只有少量存活. 因此選用複製演算法, 只需要付出少量存活物件的複製成本就可以完成

老年代: 因為物件存活率高、沒有額外空間對它進行分配擔保, 就必須採用 “標記—清理”或“標記—整理”演算法來進行回收, 不必進行記憶體複製, 且直接騰出空閒記憶體.

堆的重要資訊基本上都已經涵蓋了,那麼我們在用的java虛擬機器他是怎麼選擇對應的垃圾收集演算法呢?基於什麼區選擇的呢?下章給大家介紹下java裡的垃圾收集器。