轉載請註明原文連接: https://www.jianshu.com/p/741...
AdaptiveSizePolicy(自適應大小策略) 是 JVM GC Ergonomics(自適應調節策略) 的一部分。html
若是開啓 AdaptiveSizePolicy,則每次 GC 後會從新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程當中統計的 GC 時間、吞吐量、內存佔用量。java
開啓 AdaptiveSizePolicy 的參數爲:web
-XX:+UseAdaptiveSizePolicy
JDK 1.8 默認使用 UseParallelGC 垃圾回收器,該垃圾回收器默認啓動了 AdaptiveSizePolicy。算法
AdaptiveSizePolicy 有三個目標:緩存
AdaptiveSizePolicy 爲了達到三個預期目標,涉及如下操做:oracle
注:AdaptiveSizePolicy 涉及的內容比較廣,本文主要關注 AdaptiveSizePolicy 對年輕代大小的影響,以及隨之產生的問題。ide
AdaptiveSizePolicy 看上去很智能,但有時它也很調皮,會引起 GC 問題。函數
某一天,有一位羣友在羣裏發來一張 jmap -heap 內存使用狀況圖。工具
說 Survivor 區佔比老是在 98% 以上。post
仔細觀察這張圖,其中包含幾個重要信息:
此外,還能夠看到 Eden、From、To 之間的比例不是默認的 8:1:1。
因而,立馬就想到 AdaptiveSizePolicy。
經羣友的確認,使用的是 JDK 1.8 的默認回收算法。
JVM 參數配置以下:
參數中沒有對 GC 算法進行配置,即便用默認的 UseParallelGC。
用默認參數啓動一個基於 JDK 1.8 的應用,而後使用 jinfo -flags pid 便可查看默認配置的 GC 算法。
上文提到,該算法默認開啓 AdaptiveSizePolicy。
即便 SurvivorRatio 的默認值是 8,但年輕代三個區域之間的比例仍會變更。
這個問題,能夠參考來自R大的回答:
http://hllvm.group.iteye.com/...
HotSpot VM裏,ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)默認行爲是SurvivorRatio若是不顯式設置就沒啥用。顯式設置到跟默認值同樣的值則會有效果。由於ParallelScavenge系的GC最初設計就是默認打開AdaptiveSizePolicy的,它會自動、自適應的調整各類參數。
在羣友的截圖中,From 區只有 10M,Eden 區佔用了卻超過年輕代八成的空間。
其緣由是 AdaptiveSizePolicy 爲了達到指望的目標而進行了調整。
大概定位了 Survivor 區小的緣由,還有一個問題:
爲何老年代的佔比和使用量都比較高?
因而羣友使用 jmap -histo 查看堆中的實例。
能夠看出,其中有兩個類的實例比較多,分別是:
因而,搜索關鍵類 ExpiringCache。
能夠看出在 ExpiringCache 的構造函數中,初始化了一個 LinkedHashMap。
懷疑 LinkedHashMap$Entry 數量多的緣由和 ExpiringCache$Entry 直接有關。
ExpiringCache(long millisUntilExpiration) { this.millisUntilExpiration = millisUntilExpiration; map = new LinkedHashMap<String,Entry>() { protected boolean removeEldestEntry(Map.Entry<String,Entry> eldest) { return size() > MAX_ENTRIES; } }; }
注:該 map 用於保存緩存數據,設置了淘汰機制。當 map 大小超過 MAX_ENTRIES = 200 時,會開始淘汰。
接着查看 ExpiringCache$Entry 類。
這個類的主要屬性是「時間戳」和「值」,時間戳用於超時淘汰(緩存經常使用手法)。
static class Entry { private long timestamp; private String val; …… }
接着查看哪裏使用到了這個緩存。
因而找到 get 方法,定位到只有一個類的一個方法使用到了這個緩存。
接着往上層找,看到了一個熟悉的類:File,它的 getCanonicalPath() 方法使用到了這個緩存。
該方法用於獲取文件路徑。
因而,詢問羣友,是否在項目中使用了 getCanonicalPath() 方法。
獲得的回答是確定的。
當項目中使用 getCanonicalPath() 方法獲取文件路徑時,會發生如下的事情:
老年代內存佔用量高的問題也定位到了。
由於每次 YGC 只有 200 個實例進入到老年代,問題顯得比較溫和。
只是隔一段時間觸發 FGC,應用運行看似正常。
接着使用 jstat -gcutil 查看 GC 狀況。
能夠看到從應用啓動,一共發生了 15654 次 YGC。
推算每次 YGC 有 200 個 ExpiringCache$Entry 對象進入老年代。
那麼,老年代中大約存在 3130800 個 ExpiringCache$Entry 對象。
從以前的 jmap -histo 結果中看到,ExpiringCache$Entry 對象的數量是 6118824 個。
兩個數目都爲百萬級。其他約 300W 個實例應該都在 Eden 區。
每一次 YGC 後,都會有大量的 ExpiringCache$Entry 對象被回收。
從羣友截取的 GC log 中能夠看出,YGC 的頻率大概爲 23 秒一次。
假設運行的 jmap -histo 命令是在即將觸發 YGC 以前。
那麼,應用大概在 20s 的事件內產生了 300W 個 ExpiringCache$Entry 實例,1s 內產生約 15W 個。
假設單機 QPS = 300,一次請求產生的 ExpiringCache$Entry 實例數約爲 500 個。
猜想是在循環體中使用了 getCanonicalPath() 方法。
至此能夠得出 Survior 區變小,老年代佔比變高的緣由:
從羣友的 jstat -gcutil 截圖中還能夠看出,應用從啓動到使用該命令,觸發了 19 次 FGC,一共耗時 9.933s,平均每次 FGC 耗時爲 520ms。
這樣的停頓時間,對於一個高 QPS 的應用是沒法忍受的。
定位到了問題的緣由,解決方案比較簡單。
解決的思路有兩個:
解決方案一:
不使用緩存。
使用 -Dsun.io.useCanonCaches = false
參數便可關閉緩存。
這種方案解決比較方便,但這個參數並不是常規參數,慎用。
解決方案二:
保持使用 UseParallelGC,顯式設置 -XX:SurvivorRatio=8。
配置參數進行測試:
看到默認配置下,三者之間的比例不是 8:1:1。
能夠看到,加上參數 -Xmn100m -XX:SurvivorRatio=8 參數後,固定了 Eden 和 Survivor 之間的比例。
解決方案三:
使用 CMS 垃圾回收器。
CMS 默認關閉 AdaptiveSizePolicy。
配置參數 -XX:+UseConcMarkSweepGC,經過 jinfo 命令查看,能夠看到 CMS 默認減去/不使用 AdaptiveSizePolicy。
羣友也是採用了這個方法:
能夠看出,Eden 和 Survivor 之間的比例被固定,To 區沒有被縮小。老年代的使用量和使用率也都很正常。
注:如下源碼均主要基於 openjdk 8,不一樣 jdk 版本之間會有區別。
對源碼的理解程度有限,對源碼的理解也一直在路上。
有任何錯誤,還請各位指正,謝謝。
首先解釋,爲何在 UseParallelGC 回收器的前提下,顯式配置 SurvivorRatio 便可固定年輕代三個區域之間的比例。
在 arguments.cpp 類中有一個 set_parallel_gc_flags() 方法。
從方法命名來看,是爲了設置並行回收器的參數。
// If InitialSurvivorRatio or MinSurvivorRatio were not specified, but the // SurvivorRatio has been set, reset their default values to SurvivorRatio + // 2. By doing this we make SurvivorRatio also work for Parallel Scavenger. // See CR 6362902 for details. if (!FLAG_IS_DEFAULT(SurvivorRatio)) { if (FLAG_IS_DEFAULT(InitialSurvivorRatio)) { FLAG_SET_DEFAULT(InitialSurvivorRatio, SurvivorRatio + 2); } if (FLAG_IS_DEFAULT(MinSurvivorRatio)) { FLAG_SET_DEFAULT(MinSurvivorRatio, SurvivorRatio + 2); } }
當顯式設置 SurvivorRatio,即 !FLAG_IS_DEFAULT(SurvivorRatio),該方法會設置別的參數。
方法註釋上寫着:
make SurvivorRatio also work for Parallel Scavenger
經過顯式設置 SurvivorRatio 參數,SurvivorRatio 就會在 Parallel Scavenge 回收器中生效。
至於爲什麼會生效,還有待進一步學習。
而默認是會被 AdaptiveSizePolicy 調整的。
接着查看 AdaptiveSizePolicy 動態調整內存大小的代碼。
JDK 1.8 默認的 UseParallelGC 回收器,其對應的年輕代回收算法是 Parallel Scavenge。
觸發 GC 的緣由有多種,最普通的一種是在年輕代分配內存失敗。
UseParallelGC 分配內存失敗引起 GC 的入口位於
vmPSOperations.cpp 類的 VM_ParallelGCFailedAllocation::doit() 方法。
以後依次調用瞭如下方法:
parallelScavengeHeap.cpp 類的 failed_mem_allocate(size_t size) 方法。
psScavenge.cpp 類的 invoke()、invoke_no_policy() 方法。
invoke_no_policy() 方法中有一段代碼涉及 AdaptiveSizePolicy。
if (UseAdaptiveSizePolicy) { …… size_policy->compute_eden_space_size(young_live, eden_live, cur_eden, max_eden_size, false /* not full gc*/); …… }
在 GC 主過程完成後,若是開啓 UseAdaptiveSizePolicy 則會從新計算 Eden 區的大小。
在 compute_eden_space_size 方法中,有幾個判斷。
對應 AdaptiveSizePolicy 的三個目標:
if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) || (_avg_major_pause->padded_average() > gc_pause_goal_sec())) { adjust_eden_for_pause_time(is_full_gc, &desired_promo_size, &desired_eden_size); } else if (_avg_minor_pause->padded_average() > gc_minor_pause_goal_sec()) { adjust_eden_for_minor_pause_time(is_full_gc, &desired_eden_size); } else if(adjusted_mutator_cost() < _throughput_goal) { assert(major_cost >= 0.0, "major cost is < 0.0"); assert(minor_cost >= 0.0, "minor cost is < 0.0"); adjust_eden_for_throughput(is_full_gc, &desired_eden_size); } else { if (UseAdaptiveSizePolicyFootprintGoal && young_gen_policy_is_ready() && avg_major_gc_cost()->average() >= 0.0 && avg_minor_gc_cost()->average() >= 0.0) { size_t desired_sum = desired_eden_size + desired_promo_size; desired_eden_size = adjust_eden_for_footprint(desired_eden_size, desired_sum); } }
詳細看其中一個判斷。
if ((_avg_minor_pause->padded_average() > gc_pause_goal_sec()) || (_avg_major_pause->padded_average() > gc_pause_goal_sec()))
若是統計的 YGC 或者 Old GC 時間超過了目標停頓時間,則會調用 adjust_eden_for_pause_time 調整 Eden 區大小。
gc_pause_goal_sec() 方法獲取預期停頓時間,在 ParallelScavengeHeap::initialize() 方法中,經過讀取 JVM 參數 MaxGCPauseMillis 獲取。
接下來,再看 CMS 回收器。
CMS 初始化分代位於 cmsCollectorPolicy.cpp 類的 initialize_generations() 方法。
if (UseParNewGC) { if (UseAdaptiveSizePolicy) { _generations[0] = new GenerationSpec(Generation::ASParNew, _initial_gen0_size, _max_gen0_size); } else { _generations[0] = new GenerationSpec(Generation::ParNew, _initial_gen0_size, _max_gen0_size); } } else { _generations[0] = new GenerationSpec(Generation::DefNew, _initial_gen0_size, _max_gen0_size); } if (UseAdaptiveSizePolicy) { _generations[1] = new GenerationSpec(Generation::ASConcurrentMarkSweep, _initial_gen1_size, _max_gen1_size); } else { _generations[1] = new GenerationSpec(Generation::ConcurrentMarkSweep, _initial_gen1_size, _max_gen1_size); }
其中 _generations[0] 表明年輕代特徵,_generations[1] 表明老年代特徵。
若是設置不一樣的 UseParNewGC 、UseAdaptiveSizePolicy 參數,會對年輕代和老年代使用不一樣的策略。
CMS 垃圾回收入口位於 genCollectedHeap.cpp 類的 do_collection 方法。
在 do_collection 方法中,GC 主過程完成後,會對每一個分代進行大小調整。
for (int j = max_level_collected; j >= 0; j -= 1) { // Adjust generation sizes. _gens[j]->compute_new_size(); }
本文主要討論 AdaptiveSizePolicy 對年輕代的影響,主要看 ASParNewGeneration 類,其中的 AS 前綴就是 AdaptiveSizePolicy 的意思。
若是設置 -XX:+UseAdaptiveSizePolicy 則年輕代對應 ASParNewGeneration 類,不然對應 ParNewGeneration 類。
在 ASParNewGeneration 類中 compute_new_size() 方法中,調用了另外一個方法調整 Eden 區大小。
size_policy->compute_eden_space_size(eden()->capacity(), max_gen_size());
該方法與 Parallel Scavenge 的 compute_eden_space_size 方法相似,也從三個方面對內存大小進行調整,分別是:
接着進行測試,設置參數 -XX:+UseAdaptiveSizePolicy、
-XX:+UseConcMarkSweepGC。
指望 CMS 會啓用 AdaptiveSizePolicy,但根據 jmap -heap 結果查看,並無啓動,年輕代三個區域之間的比例爲 8:1:1。
從 jinfo 命令結果也能夠看出,即便設置了 -XX:+UseAdaptiveSizePolicy,仍然關閉了 AdaptiveSizePolicy。
由於在 JDK 1.8 中,若是使用 CMS,不管 UseAdaptiveSizePolicy 如何設置,都會將 UseAdaptiveSizePolicy 設置爲 false。
查看 arguments.cpp 類中的 set_cms_and_parnew_gc_flags 方法,其調用了 disable_adaptive_size_policy 方法將 UseAdaptiveSizePolicy 設置成 false。
static void disable_adaptive_size_policy(const char* collector_name) { if (UseAdaptiveSizePolicy) { if (FLAG_IS_CMDLINE(UseAdaptiveSizePolicy)) { warning("disabling UseAdaptiveSizePolicy; it is incompatible with %s.", collector_name); } FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false); } }
若是是在啓動參數中設置了,則會打出提醒。
但在 JDK 1.6 和 1.7 中,set_cms_and_parnew_gc_flags 方法的邏輯和 1.8 中的不一樣。
若是 UseAdaptiveSizePolicy 參數是默認的,則強制設置成 false。
若是顯式設置(complete),則不作改變。
// Turn off AdaptiveSizePolicy by default for cms until it is // complete. if (FLAG_IS_DEFAULT(UseAdaptiveSizePolicy)) { FLAG_SET_DEFAULT(UseAdaptiveSizePolicy, false); }
因而嘗試使用 JDK 1.6 搭建 web 應用,加上 -XX:+UseAdaptiveSizePolicy、-XX:+UseConcMarkSweepGC 兩個參數。
再用 jinfo -flag 查看,看到兩個參數都被置爲 true。
接着,使用 jmap -heap 查看堆內存使用狀況,發現展現不了信息。
這實際上是 JDK 低版本的一個 Bug。
1.6.30以上到1.7的所有版本已經確認有該問題,jdk8修復。
參考:UseAdaptiveSizePolicy與CMS垃圾回收同時使用致使的JVM報錯 https://www.cnblogs.com/moona...