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ù)是怎么解析的。

-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識別出來的呢?下面來分析一下。

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

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

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è)置全局變量:

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ù)來完成的:

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)建堆的工作;

創(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è)置。比如下面的代碼片段:

_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ù);

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

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