目錄
上一節我們瞭解到JVM的運行時數據區的5個模塊,今天重點講一下JVM的堆內存模型。這些都是大廠面試必備的哦,同學們請注意聽講
一、堆的內存區域
1.1 堆內存區域介紹
在jvm的堆內存中有三個區域:
- 年輕代:用於存放新產生的對象。
- 老年代:用於存放被長期引用的對象。
- 持久帶:用於存放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的對象包括:
- 方法區: 類靜態屬性引用的對象;
- 方法區: 常量引用的對象;
- 虛擬機棧(本地變量表)中引用的對象.
- 本地方法棧JNI(Native方法)中引用的對象。
注: 即使在可達性分析算法中不可達的對象, VM也並不是馬上對其回收, 因爲要真正宣告一個對象死亡, 至少要經歷兩次標記過程: 第一次是在可達性分析後發現沒有與GC Roots相連接的引用鏈, 第二次是GC對在F-Queue執行隊列中的對象進行的小規模標記(對象需要覆蓋finalize()方法且沒被調用過).
三、GC原理-垃圾回收算法
Java與C++等語言最大的技術區別:自動化的垃圾回收機制(GC),那麼爲什麼要了解GC和內存分配策略呢?
- 面試需要
- GC對應用的性能是有影響的;
- 寫代碼有好處
棧:棧中的生命週期是跟隨線程,所以一般不需要關注
堆:堆中的對象是垃圾回收的重點
方法區/元空間:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點
目前爲止,jvm已經發展處四種比較成熟的垃圾收集算法:
- 標記-清除算法;
- 複製算法;
- 標記-整理算法;
- 分代收集算法
3.1 標記-清除算法
這種垃圾回收一次回收分爲兩個階段:標記、清除。首先標記所有需要回收的對象,在標記完成後回收所有被標記的對象。這種回收算法會產生大量不連續的內存碎片,當要頻繁分配一個大對象時,jvm在新生代中找不到足夠大的連續的內存塊,會導致jvm頻繁進行內存回收(目前有機制,對大對象,直接分配到老年代中)
優點
- 利用率百分之百
缺點
- 標記和清除的效率都不高(比對複製算法)
- 會產生大量的不連續的內存碎片
3.2 複製算法
這種算法會將內存劃分爲兩個相等的塊,每次只使用其中一塊。當這塊內存不夠使用時,就將還存活的對象複製到另一塊內存中,然後把這塊內存一次清理掉。這樣做的效率比較高,也避免了內存碎片。但是這樣內存的可使用空間減半,是個不小的損失。
優點
- 簡單高效,不會出現內存碎片問題
缺點
- 內存利用率低,只有一半
- 存活對象較多時效率明顯會降低
3.3 標記-整理算法
這是標記-清除算法的升級版。在完成標記階段後,不是直接對可回收對象進行清理,而是讓存活對象向着一端移動,然後清理掉邊界以外的內存
優點
- 利用率百分之百
- 沒有內存碎片
缺點
- 標記和清除的效率都不高
- 效率相對標記-清除要低
3.4.分代收集算法
當前商業虛擬機都採用這種算法。首先根據對象存活週期的不同將內存分爲幾塊即新生代、老年代,然後根據不同年代的特點,採用不同的收集算法
新生代: 每次垃圾收集都能發現大批對象已死, 只有少量存活. 因此選用複製算法, 只需要付出少量存活對象的複製成本就可以完成
老年代: 因爲對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用“標記—清理”或“標記—整理”算法來進行回收, 不必進行內存複製, 且直接騰出空閒內存.
堆的重要信息基本上都已經涵蓋了,那麼我們在用的java虛擬機他是怎麼選擇對應的垃圾收集算法呢?基於什麼區選擇的呢?下章給大家介紹下java裏的垃圾收集器。