GC part 6

part 6

JVM參數(shù)解析以及Heap初始化過程分析

在create_vm的時候,我們設(shè)置的JVM參數(shù)會被解析出來,然后生成各種策略,比如設(shè)置了 -XX:+UseSerialGC,那么JVM就會適應Serial GC來作為堆的管理者,當然,也就會初始化新生代和老年代,不同的參數(shù)設(shè)置會生成不同的GC策略,JVM參數(shù)眾多,不同參數(shù)之間有可能互相影響,有些參數(shù)可能導致非常詭異的現(xiàn)象,所以在設(shè)置JVM參數(shù)的時候,如果對一個參數(shù)并不是很了解,不要輕易設(shè)置。本文將從JVM參數(shù)解析開始說起,然后會分析一下堆的初始化,分析堆的初始化的過程也就是去分析JVM是如何使用我們設(shè)置的JVM參數(shù)的過程。

JVM參數(shù)解析

Arguments::parse(args)函數(shù)是JVM參數(shù)解析的入口,在thread.cpp里面的create_vm函數(shù)里面可以找到這個函數(shù)調(diào)用,因為JVM可配置的參數(shù)特別多,所以本文不打算將所有的JVM參數(shù)都講一下,下面的文章將只是介紹一下JVM解析參數(shù)的流程,會拿幾個參數(shù)來具體分析其解析的流程;parse_vm_init_args是我比較關(guān)注的函數(shù),類似于-XX:+UseSerialGC這樣的參數(shù)將在從這里開始進行解析,當然,具體的解析是在parse_each_vm_init_arg里面完成的,所以直接來關(guān)注parse_each_vm_init_arg函數(shù);下面來看看JVM參數(shù)-Xms,-Xmx以及類似于-XX:+UseConcMarkSweepGC這樣的參數(shù)是怎么解析的。

image

-Xms用于設(shè)置堆的最小容量,-Xmx(或者-XX:MaxHeapSize)用于設(shè)置堆的最大容量,如果-Xms設(shè)置的大小和-Xmx一樣大,那么堆就是不可擴展的,否則堆就是可以動態(tài)擴展的;上面的代碼片段就是用來解析-Xms和-Xmx兩個參數(shù)的,解析好的參數(shù)會設(shè)定到相應的全局共享變量中去,比如-Xms就會被設(shè)置到InitialHeapSize中去,-Xmx會設(shè)置到MaxHeapSize中去;這兩個是數(shù)值型的參數(shù),下面來看一個flag類型的參數(shù)設(shè)置,比如我們使用-XX:+UseConcMarkSweepGC,那么這個參數(shù)是怎么被JVM識別出來的呢?下面來分析一下。

image

還是在同樣的函數(shù)里面解析,上面的代碼片段是解析類似-XX:+UseConcMarkSweepGC參數(shù)的入口,parse_argument負責具體的解析工作,下面來看看parse_argument函數(shù)是怎么實現(xiàn)解析這樣的參數(shù)并且設(shè)置到全局變量中去的。parse_argument函數(shù)可以在process_argument中找到;

image

圖中標注的就是解析的關(guān)鍵,+%或者-%用于匹配-XX:+UseSerialGC中的+,下面可以看一個實際的啟動時參數(shù)解析例子:

image

set_bool_flag會將解析到的flag對應的全局變量設(shè)置為true,可以具體看看set_bool_flag函數(shù)是如何做到這一點的。

static bool set_bool_flag(const char* name, bool value, Flag::Flags origin) {
  if (CommandLineFlags::boolAtPut(name, &value, origin) == Flag::SUCCESS) {
    return true;
  } else {
    return false;
  }
}

Flag::Error CommandLineFlags::boolAtPut(const char* name, size_t len, bool* value, Flag::Flags origin) {
  Flag* result = Flag::find_flag(name, len);
  return boolAtPut(result, value, origin);
}

find_flag將找到對應的flag信息,可以在下面的debug界面中看到找到了我們設(shè)置的flag,找到flag之后會調(diào)用boolAtPut函數(shù)來設(shè)置全局變量:

image
Flag::Error CommandLineFlags::boolAtPut(Flag* flag, bool* value, Flag::Flags origin) {
  const char* name;
  if (flag == NULL) return Flag::INVALID_FLAG;
  if (!flag->is_bool()) return Flag::WRONG_FORMAT;
  name = flag->_name;
  Flag::Error check = apply_constraint_and_check_range_bool(name, *value, !CommandLineFlagConstraintList::validated_after_ergo());
  if (check != Flag::SUCCESS) return check;
  bool old_value = flag->get_bool();
  trace_flag_changed<EventBooleanFlagChanged, bool>(name, old_value, *value, origin);
  check = flag->set_bool(*value);
  *value = old_value;
  flag->set_origin(origin);
  return check;
}

在設(shè)置了JVM參數(shù)之后,我們也不知道參數(shù)這樣設(shè)置是否存在問題,或者是否有沖突,但是JVM必須能夠發(fā)現(xiàn)這種沖突,并且及時給出提示,check_vm_args_consistency函數(shù)將完成JVM參數(shù)設(shè)置校驗的工作,比如校驗GC設(shè)置是否合理是通過調(diào)用check_gc_consistency函數(shù)來完成的:

image

JVM參數(shù)的解析部分析就到這里,但是還是得說一下,為什么有時候我們什么參數(shù)也不配置,JVM也能運行起來呢?Arguments::apply_ergo()就是做這個工作的,它會進行一些自動的配置來啟動JVM,比如選擇GC等,select_gc就是做這件事情的:

void Arguments::select_gc() {
  if (!gc_selected()) {
    select_gc_ergonomically();
    if (!gc_selected()) {
      vm_exit_during_initialization("Garbage collector not selected (default collector explicitly disabled)", NULL);
    }
  }
}

gc_selected首先判斷是否設(shè)置了GC,判斷條件很簡單:

bool Arguments::gc_selected() {
#if INCLUDE_ALL_GCS
  return UseSerialGC || UseParallelGC || UseParallelOldGC || UseConcMarkSweepGC || UseG1GC;
#else
  return UseSerialGC;
#endif // INCLUDE_ALL_GCS
}

如果沒有設(shè)置,select_gc_ergonomically將選擇一個合適的GC,在java9里面的實現(xiàn)如下:

void Arguments::select_gc_ergonomically() {
  if (os::is_server_class_machine()) {
    if (!UseAutoGCSelectPolicy) {
       FLAG_SET_ERGO_IF_DEFAULT(bool, UseG1GC, true);
    } else {
      if (should_auto_select_low_pause_collector()) {
        FLAG_SET_ERGO_IF_DEFAULT(bool, UseConcMarkSweepGC, true);
        FLAG_SET_ERGO_IF_DEFAULT(bool, UseParNewGC, true);
      } else {
        FLAG_SET_ERGO_IF_DEFAULT(bool, UseParallelGC, true);
      }
    }
  } else {
    FLAG_SET_ERGO_IF_DEFAULT(bool, UseSerialGC, true);
  }
}

選擇的策略和當前JVM的Mode有關(guān),如果是client模式,則默認選擇SerialGC,這也是Client模式下的最優(yōu)的GC;如果是在Server模式下,那么如果沒有設(shè)置UseAutoGCSelectPolicy的話,就默認使用G1(所以說java9默認的GC是G1),如果設(shè)置了UseAutoGCSelectPolicy,那么根據(jù)should_auto_select_low_pause_collector的結(jié)果來選擇;

bool Arguments::should_auto_select_low_pause_collector() {
  if (UseAutoGCSelectPolicy &&
      !FLAG_IS_DEFAULT(MaxGCPauseMillis) &&
      (MaxGCPauseMillis <= AutoGCSelectPauseMillis)) {
    return true;
  }
  return false;
}

如果should_auto_select_low_pause_collector返回true,那么就選擇CMS,否則使用UseParallelGC;前者是相應時間優(yōu)先GC,后者則是吞吐量優(yōu)先GC。

JVM堆的初始化

JVM參數(shù)解析之后,在初始化JVM堆的時候就可以使用我們設(shè)置的JVM參數(shù)了,不同的參數(shù)使用的堆是不一樣的,GC策略也是有所差異的,下面來分析一下堆的初始化過程;initialize_heap函數(shù)用于初始化堆,下面簡單分幾個步驟分析一下這個函數(shù)具體做了些什么工作。

  • (1)、創(chuàng)建堆

首先要做的事情就是要創(chuàng)建使用的堆,創(chuàng)建哪種類型的堆和設(shè)置的GC參數(shù)有關(guān),create_heap函數(shù)將完成創(chuàng)建堆的工作;

image

創(chuàng)建什么類型的堆依賴于選擇了什么類型的GC,JVM提供了四種類型的GC,分別是并行GC(UseParallelGC),也就是使用多線程來做GC,G1 (UseG1GC),CMS以及串行GC(UseSerialGC);Universe::create_heap_with_policy函數(shù)用于創(chuàng)建對應的堆,它的兩個泛型類型,一個是堆的類型Heap,一個是管理堆的策略Policy,比如對于UseSerialGC,那么創(chuàng)建的堆就是GenCollectedHeap,堆管理的策略就是MarkSweepPolicy;在HotSpot中,堆的實現(xiàn)是一種典型的分代實現(xiàn),簡單來說分為新生代和老年代,不同的分代存放的對象具有不一樣的特征,但是不同特征的對象也可能放在一起,分在不同分代中的特征包括對象的GC年齡以及對象的大小等因素,對象將優(yōu)先在Eden中存活,經(jīng)過多次Minor GC依然存活的對象將晉升(Promotion)到老年代,但是晉升可能失敗,所以有部分本該晉升到老年代的對象依然存活在新生代,而在做Minor GC的時候,如果Eden + From中存活的對象無法拷貝到To區(qū)域,那么也會直接轉(zhuǎn)移到老年代,這稱為提前晉升,還有一些比較大的對象會直接在老年代申請空間;下面的文章將以UseSerialGC為例,看看堆創(chuàng)建的后續(xù)流程。

先來看一下create_heap_with_policy函數(shù)的實現(xiàn):

template <class Heap, class Policy>
CollectedHeap* Universe::create_heap_with_policy() {
  Policy* policy = new Policy();
  policy->initialize_all();
  return new Heap(policy);
}

對于UseSerialGC來說,policy就是MarkSweepPolicy,Heap就是GenCollectedHeap;下面分別看看策略的初始化和堆的初始化。

Policy初始化

initialize_all函數(shù)應該是我們應該主要關(guān)心的,這個函數(shù)在基類GenCollectorPolicy中實現(xiàn):

  virtual void initialize_all() {
    CollectorPolicy::initialize_all();
    initialize_generations();
  }

CollectorPolicy::initialize_all()函數(shù)的實現(xiàn)在CollectorPolicy里面,實現(xiàn)如下:

  virtual void initialize_all() {
    initialize_alignments();
    initialize_flags();
    initialize_size_info();
  }

initialize_alignments會根據(jù)os的page大小來設(shè)置空間對齊參數(shù),稍后會根據(jù)這些對齊參數(shù)來將我們設(shè)置的各種堆大小對齊到合理的值,所以JVM里面的實際堆大小并不會精確的等于我們設(shè)置的大小,而是會做對齊操作;
initialize_flags的工作是根據(jù)我們設(shè)定的JVM參數(shù)來設(shè)置一些全局變量的值,這里的設(shè)置是"修正"設(shè)置,在參數(shù)解析的時候已經(jīng)設(shè)置過了,但是現(xiàn)在某些參數(shù)需要被重寫設(shè)置,比如堆的大小參數(shù),需要對齊一下大小再重新設(shè)置。比如下面的代碼片段:

image

_min_heap_byte_size表示堆的最小值,align_size_up函數(shù)用于對齊堆的大??;aligned_initial_heap_size是對齊之后的堆初始化大小,如果和InitialHeapSize大小不一樣,就要重新設(shè)置一下InitialHeapSize;MaxHeapSize也是同樣的處理方法;initialize_size_info函數(shù)相對來說比較復雜,它的工作就是確定新生代和老年代的堆大小,比如新生代的初始化堆大小,以及最大堆大小等信息,下面看看細節(jié):

// Determine maximum size of the young generation.

  if (FLAG_IS_DEFAULT(MaxNewSize)) {
    _max_young_size = scale_by_NewRatio_aligned(_max_heap_byte_size);
    // Bound the maximum size by NewSize below (since it historically
    // would have been NewSize and because the NewRatio calculation could
    // yield a size that is too small) and bound it by MaxNewSize above.
    // Ergonomics plays here by previously calculating the desired
    // NewSize and MaxNewSize.
    _max_young_size = MIN2(MAX2(_max_young_size, _initial_young_size), MaxNewSize);
  }

這段代碼要確定_max_young_size的大小,也就是新生代的大小,如果我們使用-Xmn設(shè)置了新生代的大小,那么就不用執(zhí)行這段代碼,否則就要通過scale_by_NewRatio_aligned函數(shù)來確定新生代的大小,scale_by_NewRatio_aligned的實現(xiàn)如下:

size_t GenCollectorPolicy::scale_by_NewRatio_aligned(size_t base_size) {
  return align_size_down_bounded(base_size / (NewRatio + 1), _gen_alignment);
}

我們可以使用-XX:NewRatio來設(shè)置新生代的占用整個堆的比例,NewRatio默認為2,也就是young_gen_size = heap_size / (NewRatio + 1);接著看下面的代碼:

  if (_max_heap_byte_size == _initial_heap_byte_size) {
    // The maximum and initial heap sizes are the same so the generation's
    // initial size must be the same as it maximum size. Use NewSize as the
    // size if set on command line.
    _max_young_size = FLAG_IS_CMDLINE(NewSize) ? NewSize : _max_young_size;
    _initial_young_size = _max_young_size;

    // Also update the minimum size if min == initial == max.
    if (_max_heap_byte_size == _min_heap_byte_size) {
      _min_young_size = _max_young_size;
    }
  }

如果堆不可擴展,也就是-Xms和-Xmx是相等的,那么就會執(zhí)行這段代碼,_max_young_size會根據(jù)是否設(shè)定了NewSize來確定,如果設(shè)定了那就取設(shè)定的NewSize(-Xmn),接著_initial_young_size會被設(shè)定了_max_young_size,也就是新生代不可擴展了;這里稍微說一下,DefNew不會進行堆擴展,如果Eden無法滿足申請空間的要求的時候,他就會嘗試去From去申請內(nèi)存;如果堆可擴展,那么就會執(zhí)行下面的代碼:

if (FLAG_IS_CMDLINE(NewSize)) {
      // If NewSize is set on the command line, we should use it as
      // the initial size, but make sure it is within the heap bounds.
      _initial_young_size =
        MIN2(_max_young_size, bound_minus_alignment(NewSize, _initial_heap_byte_size));
      _min_young_size = bound_minus_alignment(_initial_young_size, _min_heap_byte_size);
    } else {
      // For the case where NewSize is not set on the command line, use
      // NewRatio to size the initial generation size. Use the current
      // NewSize as the floor, because if NewRatio is overly large, the resulting
      // size can be too small.
      _initial_young_size =
        MIN2(_max_young_size, MAX2(scale_by_NewRatio_aligned(_initial_heap_byte_size), NewSize));
    }

至此,新生代_min_young_size、_initial_young_size、_max_young_size都已經(jīng)確定了,下面就是確定老年代的這三個變量;這部分內(nèi)容就不再贅述了,后續(xù)再專門研究吧。
執(zhí)行完CollectorPolicy::initialize_all()之后,initialize_generations就要被執(zhí)行,這個函數(shù)為創(chuàng)建和初始化堆進行準備,對于MarkSweepPolicy來說實現(xiàn)如下:

void MarkSweepPolicy::initialize_generations() {
  _young_gen_spec = new GenerationSpec(Generation::DefNew, _initial_young_size, _max_young_size, _gen_alignment);
  _old_gen_spec   = new GenerationSpec(Generation::MarkSweepCompact, _initial_old_size, _max_old_size, _gen_alignment);
}

可以看到新生代是DefNew,老年代是MarkSweepCompact,上面計算好的新生代老年代的堆大小也被設(shè)置到GenerationSpec對象中了,后續(xù)會使用這些參數(shù)來創(chuàng)建具體的堆以及初始化堆空間。

Heap初始化

下面以GenCollectedHeap為例看看堆是如何初始化的;在initialize_heap中調(diào)用create_heap之后,就會調(diào)用創(chuàng)建好的堆的initialize函數(shù)來初始化堆,對應著看GenCollectedHeap的initialize函數(shù);

image

主要看標記出來的兩行代碼,分別初始化了新生代和老年代,gen_policy()->young_gen_spec()函數(shù)將返回上面設(shè)定的GenerationSpec,然后init函數(shù)將根據(jù)具體的堆類型進行創(chuàng)建新生代和老年代并且初始化;

image

比如對于+UseSerialGC,新生代就是DefNew,老年代就是MarkSweepCompact;下面看看新生代是如何進行初始化的,DefNewGeneration::DefNewGeneration這個構(gòu)造函數(shù)將用來創(chuàng)建一個DefNew,下圖展示了幾個關(guān)鍵的地方:

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • part 6 JVM參數(shù)解析以及Heap初始化過程分析 在create_vm的時候,我們設(shè)置的JVM參數(shù)會被解析出...
    一字馬胡閱讀 377評論 0 1
  • 個人專題目錄[http://www.itdecent.cn/u/2a55010e3a04] 4 JVM+GC解析...
    Java及SpringBoot閱讀 1,299評論 0 7
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡上收集的一些資料的整理,因此不免有一些不準確的地方,同時不同JDK版本的...
    高廣超閱讀 16,062評論 3 83
  • Catalog 1 怎么解決OOM?/ 怎么排查OOM?/ JVM調(diào)優(yōu)1.1 JDK自帶工具1.2 阿里開源JVM...
    allen鍋閱讀 548評論 0 1
  • JVM架構(gòu)圖分析 JVM被分為三個主要的子系統(tǒng) (1)類加載器子系統(tǒng)(2)運行時數(shù)據(jù)區(qū)(3)執(zhí)行引擎 1. 類加載...
    匆匆歲月閱讀 1,096評論 1 19

友情鏈接更多精彩內(nèi)容