1. 程式人生 > >JVM_8_記憶體分配與回收策略

JVM_8_記憶體分配與回收策略

記憶體分配與回收策略

參考資料:

《Java虛擬機器垃圾回收(四) 總結:記憶體分配與回收策略 方法區垃圾回收 以及 JVM垃圾回收的調優方法》

在之前看"分代收集演算法"的時候,我們知道目前幾乎所有商業虛擬機器的垃圾收集器都採用分代收集演算法,對於Hotspot虛擬機器年代劃分,如下圖:

物件的記憶體分配從大體上講:

在堆上分配,主要在新生代對的Eden區中分配。少數情況下,可能直接分配到年老代中。

Java技術體系中所提倡的自動記憶體管理最終可以規劃為自動解決兩個問題:給物件分配記憶體以及回收分配給物件的記憶體

下面來看看給物件分配記憶體的那些事兒。

物件的記憶體分配,往大方向上講,就是在堆上分配,物件主要分配在新生代的Eden區上。少數情況下也肯能會直接分配在年老代中

分配的規則並不是百分百固定的。其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機器中記憶體相關的引數配置。

接下來,咱們看看最普通的記憶體分配規則。

(個人感覺,可以將Eden區,粗暴的理解為年輕代)

物件優先在Eden分配

前面的文章介紹過Hotspot虛擬機器新生代記憶體佈局及演算法:

a. 將新生代記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間。

b. 每次使用Enden和其中一塊Survivor。

c. 當回收時,將Eden和使用中的Sruvivor中還存的物件一次性複製到另一塊Survivor;

d. 然後清理掉Eden和使用過的Survivor空間;

e. 後面就使用Eden和另一塊Survivior空間,重複步驟3。

預設Eden:Survivor=8:1,即每次可以使用90%的空間,只有一塊Survivor空間被浪費。

大多數情況下,物件在Eden區中分配;

Eden區沒有足夠空間進行分配時,JVM將會發起一次MinorGC(新生代GC)

MinorGC時,如果發現存活的物件無法全部放入Servivor空間,只好通過分配機制提前轉入熬年老代中

大物件直接進入年老代

所謂的大物件是指:需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列

大物件對虛擬機器的記憶體分配來說就是一個壞訊息,比遇到一個大物件更加壞的訊息就是:遇到一群"朝生夕滅"的"短命大物件"

經常出現大物件容易導致記憶體還有不少空間時,就提前觸發垃圾收集以獲取足夠的連續空間來"安置"它們。

所以我們應該避免建立大物件;

"-XX:PretenureSizeThreshold":

可以設定這個閥值,大於這個引數值的物件直接在年老代中分配。

預設為0(無效),且只對Serail 和 ParNew兩款收集器有效。

如果需要使用該引數,可以考慮ParNew+CMS組合



長期存活的物件將進入年老代

虛擬機器給每個物件定義了一個物件年齡(Age)計數器,其計算流程如下:

a. 在Enden區中分配的物件,經Minor GC之後還存活,就複製移動到Survivor區,年齡為1;

b. 而後每經歷一次Minor GC後還存活,在Survivor區複製移動一次,年齡就增加1歲

c. 如果年齡達到一定程度,就晉升到年老代中

"-XX:MaxTenuringThreshold":

設定新生代物件晉升年老代的年齡閥值,預設為15

動態物件年齡判斷

JVM為了更好適應不同程式,不是永遠要求等到MaxTenuringThreshold引數設定的年齡。

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


空間分配擔保

當Survivor空間不夠用時,需要依賴其他記憶體(年老代)進行分配擔保(Hanle Promotion)

分配擔保流程如下:

a. 在發生Minor GC之前,JVM首先檢查年老代最大可用的連續空間是否大於新生所有物件的空間

b. 如果大於,那麼可以確保Minor GC是安全的。

c. 如果不大於,則JVM檢視HandlePromotionFailure值是否允許擔保失敗。

d. 如果允許,將嘗試進行一次Minor GC,但這是有風險對的;

e. 如果小於或HandlePromotionFailure值不允許冒險,那這時,要改為進行一次Full GC;

嘗試Minor GC的風險--擔保失敗:

因為嘗試Minor GC前,無法知道存貨的物件大小,所以使用歷次晉升到年老代物件的平均大小作為經驗值。

加入嘗試的Minor GC最終存活的物件遠遠高於經驗值的話,會導致擔保失敗(Handle Promotion Failure)。

失敗後只有重新發起一次Full GC,這繞了一個大圈,代價較高。

但一般還是要開啟HandlePromotionFailure,避免Full GC過於頻繁,而且擔保失敗概率還是比較低的。

JDK1.6之後,JVM程式碼中已經不再使用HandlePromotionFailure引數了...

規則變為:

只要年老代最大可用的連續空間大於新生所有物件的空間或歷次晉升到年老代物件的平均大小,就會進行MinorGC,否則進行Full GC

即年老代最大可用的連續空間小於新生所有物件空間時,不在檢查HandlePromotionFailure,而是直接檢查歷次晉升熬年老代物件的平均大小。

回收方法區

參考資料:

《Java虛擬機器垃圾回收(四) 總結:記憶體分配與回收策略 方法區垃圾回收 以及 JVM垃圾回收的調優方法》

在<JVM_1_執行時記憶體區域>一篇中,曾介紹過方法區相關的回收問題。

雖然JVM規範規定這個區域可以不實現垃圾收集,且針對常量池和型別解除安裝的回收效果不佳,但方法區實現垃圾回收還是必要的。

方法區(永久代)的主要回收物件

1. 廢棄常量

          與回收Java堆中的物件非常類似。

2. 無用的類

          同時滿足下面3個條件才能算"無用的類"

          a. 該類所有例項都已經被回收(即Java堆中不存在該類的任何例項);

          b. 載入該類的ClassLoader已經被回收,即通過載入程式載入器載入的類不能被回收。

          c. 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的存在。

需要注意方法區回收的應用

大量使用反射、動態代理、經常動態大量類的應用,要注意類的回收;

如執行時動態生成類的應用:

1. GCLib在Spring、hibernate等框架中對類進行增強時會使用;

2. VM的動態語言也會動態建立類來實現語言的動態性;

3. 另外,JSP、基於OSGI頻繁自定義ClassLoader的應用等

Hotspot虛擬機器的相關調整

JDK1.7:

          使用永久代實現方法區,這樣就不用專門實現方法區的記憶體管理,但這樣子容易引起記憶體溢位問題;

          不在Java堆的永久代中生成分配字串的常量池,而是在Java堆其他的主要部分(年輕代和年老代)中分配。

          更多請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html

JDK1.8:

          永久代已被刪除,類元資料(Class Metadata)儲存空間在本地記憶體中分配,並顯式管理元資料的空間。

          相關引數:

                    "-XX:MaxMetaspaceSize" (JDK8):指定類元資料區的最大記憶體大小;

                    "-XX:MetaspaceSize" (JDK8):指定類元資料區的記憶體閾值--超過將觸發垃圾回收;

                    "-Xnolassgc":控制是否對類進行回收;

                    "-verbose:class"、"-XX:TraceClassLoading"、"-XX:TraceClassUnloading":檢視類載入和解除安裝資訊;

JVM垃圾回收的調優方法

記憶體回收與垃圾收集器是影響系統性能、併發能力的主要因素之一,一般都需要進行一些手動的測試、調整優化;

明確期望的目標(關注點)

首先應該明確我們的應用程式調整垃圾回收期望的目標(關注點)是什麼?

1. 停頓時間

GC停頓時間越短就越適合與使用者互動的程式,良好的響應速度能提升使用者體驗。

與使用者互動較多的場景,以給使用者帶來較好的體驗;

如常見Web、B/S系統的伺服器上的應用;

2. 吞吐量

吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間);

高吞吐量可以高效率的利用CPU時間,儘快完成運算的任務,主要適合在後臺計算而不需要太多互動的任務。

應用程式執行在具有多個CPU的機器上,對暫停時間沒有特別高的要求。

程式主要在後臺進行計算,而不需要與使用者進行太多互動。

例如 那些執行批量處理、訂單處理、工資支付、科學計算的應用程式。

3. 覆蓋區

在達到前面兩個目標的前提下,儘量減少堆的記憶體空間,以獲得更好的空間區域性性;

可以減少到不男足前兩個目標為止,然後再解決未滿足的目標;

如果是動態收縮的堆設定,堆的大小將隨著垃圾收集器試圖滿足競爭目標而振盪。

總結一下就是:

低停頓、高吞吐量、少用記憶體資源。

只是一般這些關注點都是互相影響的,增大堆記憶體空間獲得高吞吐量但會增加停頓時間,反之亦然,做不到完美,有時只能折中。

JVM自適應調整

JVM有自適應選擇、調整 相關設定的功能。

一般都會根據平臺效能選擇好垃圾收集器,並且設定好引數;

在執行中,一些垃圾收集器還會動態監控資訊,自動的、動態的調整垃圾收集策略;

所以放我們不知道如何選擇垃圾收集器和調整時,應該首先讓JVM自適應調整

讓後通過輸出GC日誌進行分析,看能不能滿足明確期望的目標(第一步)

如果不能滿足,或者通過列印設定的引數資訊,發現可以有更好的調優時,可以進行手動指定引數進行設定,並測試。

實踐調優:選擇垃圾收集器,並進行相關設定

需要明確一個觀點:

沒有最好的收集器,更沒有萬能的收集器;

選擇的只能是對居圖應用最合適的收集器;

我們都知道HotSpot有下面這些組合可以搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

到了實踐調優階段,那必須要了解每個具體收集器的行為特點、優勢和劣勢、調節引數等...

然後根據明確期望的目標,選擇應用最適合的收集器;

例如:使用Parallel Scavenge/Parallel Old組合,這是一種值得推薦的方式:

1. 只需要設定好記憶體大小(如"-Xmx"設定最大堆);

2. 然後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設定一個優化目標。

3. 那些具體細節引數的調節就由JVM自適應完成。

設定調整之後,應該通過在生產環境下進行不斷地測試,來分析是否達到到我們的目標。