国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JVM GC 之「AdaptiveSizePolicy」實戰

CoffeX / 3527人閱讀

摘要:如果開啟,則每次后會重新計算和區的大小,計算依據是過程中統計的時間吞吐量內存占用量。應用達到預期的吞吐量,即應用正常運行時間正常運行時間耗時。理論上,增大內存,可以降低的頻率,以此達到預期吞吐量。

轉載請注明原文鏈接:https://www.jianshu.com/p/741...
一、AdaptiveSizePolicy簡介

AdaptiveSizePolicy(自適應大小策略) 是 JVM GC Ergonomics(自適應調節策略) 的一部分。

如果開啟 AdaptiveSizePolicy,則每次 GC 后會重新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程中統計的 GC 時間、吞吐量、內存占用量。

開啟 AdaptiveSizePolicy 的參數為:

-XX:+UseAdaptiveSizePolicy

JDK 1.8 默認使用 UseParallelGC 垃圾回收器,該垃圾回收器默認啟動了 AdaptiveSizePolicy。

AdaptiveSizePolicy 有三個目標:

Pause goal:應用達到預期的 GC 暫停時間。

Throughput goal:應用達到預期的吞吐量,即應用正常運行時間 / (正常運行時間 + GC 耗時)。

Minimum footprint:盡可能小的內存占用量。

AdaptiveSizePolicy 為了達到三個預期目標,涉及以下操作:

如果 GC 停頓時間超過了預期值,會減小內存大小。理論上,減小內存,可以減少垃圾標記等操作的耗時,以此達到預期停頓時間。

如果應用吞吐量小于預期,會增加內存大小。理論上,增大內存,可以降低 GC 的頻率,以此達到預期吞吐量。

如果應用達到了前兩個目標,則嘗試減小內存,以減少內存消耗。

注:AdaptiveSizePolicy 涉及的內容比較廣,本文主要關注 AdaptiveSizePolicy 對年輕代大小的影響,以及隨之產生的問題。

AdaptiveSizePolicy 看上去很智能,但有時它也很調皮,會引發 GC 問題。

二、由 AdaptiveSizePolicy 引發的 GC 問題

某一天,有一位群友在群里發來一張 jmap -heap 內存使用情況圖。

說 Survivor 區占比總是在 98% 以上。

仔細觀察這張圖,其中包含幾個重要信息:

From 和 To 區都比較小,只有 10M。容量比較小,才顯得占比高。

Old 區的占比和使用量(兩個多 G)都比較高。

此外,還可以看到 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 查看堆中的實例。

可以看出,其中有兩個類的實例比較多,分別是:

LinkedHashMap$Entry

ExpiringCache$Entry

于是,搜索關鍵類 ExpiringCache。

可以看出在 ExpiringCache 的構造函數中,初始化了一個 LinkedHashMap。

懷疑 LinkedHashMap$Entry 數量多的原因和 ExpiringCache$Entry 直接有關。

ExpiringCache(long millisUntilExpiration) {
    this.millisUntilExpiration = millisUntilExpiration;
    map = new LinkedHashMap() {
        protected boolean removeEldestEntry(Map.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() 方法獲取文件路徑時,會發生以下的事情:

首先從緩存中讀取,取不到則需要生成緩存。

生成緩存需要新建 ExpiringCache$Entry 對象用于保存緩存值,這些新建的對象都會被分配到 Eden 區。

大量使用 getCanonicalPath() 方法時,緩存數量超過 MAX_ENTRIES = 200 開啟淘汰策略。原來 map 中的 ExpiringCache$Entry 對象變成垃圾對象,真正存活的 Entry 只有 200 個。

當發生 YGC 時,理論上存活的 200 個 Entry 會去往 To 區,其他被淘汰的垃圾 Entry 對象會被回收。

但由于 AdaptiveSizePolicy 將 To 區調整到只有 10MB,裝不下本該移動到 To 區的對象,只能直接移動到老年代

于是,在每次 YGC 時,會有接近 200 個存活的 ExpiringCache$Entry 對象進入到老年代。隨著緩存淘汰機制的運行,這些 Entry 對象立馬又變成垃圾。

當對象進入老年代,即使變成了垃圾,也需要等到老年代 GC 或者 FGC 才能將其回收。由于老年代容量較大,可以承受多次 YGC 給予的 200 個 ExpiringCache$Entry 對象。

于是,老年代使用量逐漸變高。

老年代內存占用量高的問題也定位到了。

因為每次 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 區變小,老年代占比變高的原因:

在默認 SurvivorRatio = 8 的情況下,沒有達到吞吐量的期望,AdaptiveSizePolicy 加大了 Eden 區的大小。From 和To 區被壓縮到只有 10M。

在項目中大量使用 getCanonicalPath() 方法,產生大量ExpiringCache$Entry 實例。

當 YGC 發生時候,由于 To 區太小,存活的 Entry 對象直接進入到老年代。老年代占用量逐漸變大。

從群友的 jstat -gcutil 截圖中還可以看出,應用從啟動到使用該命令,觸發了 19 次 FGC,一共耗時 9.933s,平均每次 FGC 耗時為 520ms。

這樣的停頓時間,對于一個高 QPS 的應用是無法忍受的。

定位到了問題的原因,解決方案比較簡單。

解決的思路有兩個:

不使用緩存,就不會生成大量 ExpiringCache$Entry 實例。

阻止 AdaptiveSizePolicy 縮小 To 區。讓 YGC 時存活的 ExpiringCache$Entry 對象都能順利進入 To 區,保留在年輕代,而不是進入老年代。

解決方案一:

不使用緩存。

使用 -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 區沒有被縮小。老年代的使用量和使用率也都很正常。

三、源碼層面了解 AdaptiveSizePolicy

注:以下源碼均主要基于 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 的三個目標:

與預期 GC 停頓時間對比。

與預期吞吐量對比。

如果達到預期,則調整內存容量。

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 方法類似,也從三個方面對內存大小進行調整,分別是:

adjust_eden_for_pause_time

adjust_eden_for_throughput

adjust_eden_for_footprint

接著進行測試,設置參數 -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...

四、問題小結

現階段大多數應用使用 JDK 1.8,其默認回收器是 Parallel Scavenge,并且默認開啟了 AdaptiveSizePolicy。

AdaptiveSizePolicy 動態調整 Eden、Survivor 區的大小,存在將 Survivor 區調小的可能。當 Survivor 區被調小后,部分 YGC 后存活的對象直接進入老年代。老年代占用量逐漸上升從而觸發 FGC,導致較長時間的 STW。

建議使用 CMS 垃圾回收器,默認關閉 AdaptiveSizePolicy。

建議在 JVM 參數中加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution,讓 GC log 更加詳細,方便定位問題。

五、參考資料

Garbage Collector Ergonomics

File,file.getPath(), getAbsolutePath(), getCanonicalPath()區別

UseAdaptiveSizePolicy與CMS垃圾回收同時使用導致的JVM報錯

JVM分析工具概述

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77153.html

相關文章

  • [譯]GC專家系列4-Apache的MaxClients設置及其對Tomcat Full GC的影響

    摘要:本文將介紹的參數的重要性以及在發生時對系統整體性能的顯著影響。我們來看下的選項在發生時會對系統帶來哪些影響。所以這些請求將會放到堆積隊列,隊列的長度是的中設置的。從而導致進程的數量超過,并觸發了操作系統進行內存交換的閥值。 原文鏈接:http://www.cubrid.org/blog/dev-platform/maxclients-in-apache-and-its-effect-o...

    DangoSky 評論0 收藏0
  • 一次有趣的局部變量GC

    摘要:前言最近在看實戰虛擬機發現書上的一個關于局部變量表挺有意思,先上代碼。主角沒有分配了一塊的堆空間,并使用局部變量引用這塊空間然后顯式進行一次。 前言 最近在看《實戰Java虛擬機》, 發現書上的一個關于局部變量表GC挺有意思,先上代碼。 主角 沒有GC public class Main { public static void reversion(){ { ...

    alogy 評論0 收藏0
  • 學習JVM必看書籍

    學習JVM的相關資料 《深入理解Java虛擬機——JVM高級特性與最佳實踐(第2版)》 showImg(https://segmentfault.com/img/bVbsqF5?w=200&h=200); 基于最新JDK1.7,圍繞內存管理、執行子系統、程序編譯與優化、高效并發等核心主題對JVM進行全面而深入的分析,深刻揭示JVM的工作原理。以實踐為導向,通過大量與實際生產環境相結合的案例展示了解...

    shaonbean 評論0 收藏0
  • Docker 監控實戰

    摘要:監控告警是運營系統最核心的功能之一,騰訊內部有一套很成熟的監控告警平臺,而且開發運維同學已經習慣這套平臺,如果我們針對容器再開發一個監控告警平臺,會花費很多精力,而且沒有太大的意義。也是一款付費監控解決方案,計劃收費方案是美分小時。 如今,越來越多的公司開始使用 Docker 了,現在來給大家看幾組數據: 2 / 3 的公司在嘗試了 Docker 后最終使用了它 也就是說 Docker...

    william 評論0 收藏0
  • Jvm系列:從一個題目簡析GC垃圾回收

    摘要:第二步分析第一個循環即表示產生的對象,后面規律相同和會直接放入區。因為優先放區,而且夠放,此時為兩者表示已用剩余。此值一般設置與相同以避免每次垃圾回收完后重新分配內存設置年輕代大小為。 一、概述 閑來有空翻翻書,撿撿一些基礎點,就當靜下心多寫字。Java基礎的東西無論怎么樣都會想到JVM,而提JVM必然想到最常見的一些點:字節碼加載,類初始化,方法執行,對象內存分配和回收,線程和鎖機制...

    Neilyo 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<