part 6
JVM参数解析以及Heap初始化过程分析
在create_vm的时候,我们设置的JVM参数会被解析出来,然后生成各种策略,比如设置了 -XX:+UseSerialGC,那么JVM就会适应Serial GC来作为堆的管理者,当然,也就会初始化新生代和老年代,不同的参数设置会生成不同的GC策略,JVM参数众多,不同参数之间有可能互相影响,有些参数可能导致非常诡异的现象,所以在设置JVM参数的时候,如果对一个参数并不是很了解,不要轻易设置。本文将从JVM参数解析开始说起,然后会分析一下堆的初始化,分析堆的初始化的过程也就是去分析JVM是如何使用我们设置的JVM参数的过程。
JVM参数解析
Arguments::parse(args)函数是JVM参数解析的入口,在thread.cpp里面的create_vm函数里面可以找到这个函数调用,因为JVM可配置的参数特别多,所以本文不打算将所有的JVM参数都讲一下,下面的文章将只是介绍一下JVM解析参数的流程,会拿几个参数来具体分析其解析的流程;parse_vm_init_args是我比较关注的函数,类似于-XX:+UseSerialGC这样的参数将在从这里开始进行解析,当然,具体的解析是在parse_each_vm_init_arg里面完成的,所以直接来关注parse_each_vm_init_arg函数;下面来看看JVM参数-Xms,-Xmx以及类似于-XX:+UseConcMarkSweepGC这样的参数是怎么解析的。
-Xms用于设置堆的最小容量,-Xmx(或者-XX:MaxHeapSize)用于设置堆的最大容量,如果-Xms设置的大小和-Xmx一样大,那么堆就是不可扩展的,否则堆就是可以动态扩展的;上面的代码片段就是用来解析-Xms和-Xmx两个参数的,解析好的参数会设定到相应的全局共享变量中去,比如-Xms就会被设置到InitialHeapSize中去,-Xmx会设置到MaxHeapSize中去;这两个是数值型的参数,下面来看一个flag类型的参数设置,比如我们使用-XX:+UseConcMarkSweepGC,那么这个参数是怎么被JVM识别出来的呢?下面来分析一下。
还是在同样的函数里面解析,上面的代码片段是解析类似-XX:+UseConcMarkSweepGC参数的入口,parse_argument负责具体的解析工作,下面来看看parse_argument函数是怎么实现解析这样的参数并且设置到全局变量中去的。parse_argument函数可以在process_argument中找到;
图中标注的就是解析的关键,+%或者-%用于匹配-XX:+UseSerialGC中的+,下面可以看一个实际的启动时参数解析例子:
set_bool_flag会将解析到的flag对应的全局变量设置为true,可以具体看看set_bool_flag函数是如何做到这一点的。
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界面中看到找到了我们设置的flag,找到flag之后会调用boolAtPut函数来设置全局变量:
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;
}
在设置了JVM参数之后,我们也不知道参数这样设置是否存在问题,或者是否有冲突,但是JVM必须能够发现这种冲突,并且及时给出提示,check_vm_args_consistency函数将完成JVM参数设置校验的工作,比如校验GC设置是否合理是通过调用check_gc_consistency函数来完成的:
JVM参数的解析部分析就到这里,但是还是得说一下,为什么有时候我们什么参数也不配置,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首先判断是否设置了GC,判断条件很简单:
bool Arguments::gc_selected() {
#if INCLUDE_ALL_GCS
return UseSerialGC || UseParallelGC || UseParallelOldGC || UseConcMarkSweepGC || UseG1GC;
#else
return UseSerialGC;
#endif // INCLUDE_ALL_GCS
}
如果没有设置,select_gc_ergonomically将选择一个合适的GC,在java9里面的实现如下:
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有关,如果是client模式,则默认选择SerialGC,这也是Client模式下的最优的GC;如果是在Server模式下,那么如果没有设置UseAutoGCSelectPolicy的话,就默认使用G1(所以说java9默认的GC是G1),如果设置了UseAutoGCSelectPolicy,那么根据should_auto_select_low_pause_collector的结果来选择;
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;前者是相应时间优先GC,后者则是吞吐量优先GC。
JVM堆的初始化
JVM参数解析之后,在初始化JVM堆的时候就可以使用我们设置的JVM参数了,不同的参数使用的堆是不一样的,GC策略也是有所差异的,下面来分析一下堆的初始化过程;initialize_heap函数用于初始化堆,下面简单分几个步骤分析一下这个函数具体做了些什么工作。
- (1)、创建堆
首先要做的事情就是要创建使用的堆,创建哪种类型的堆和设置的GC参数有关,create_heap函数将完成创建堆的工作;
创建什么类型的堆依赖于选择了什么类型的GC,JVM提供了四种类型的GC,分别是并行GC(UseParallelGC),也就是使用多线程来做GC,G1 (UseG1GC),CMS以及串行GC(UseSerialGC);Universe::create_heap_with_policy函数用于创建对应的堆,它的两个泛型类型,一个是堆的类型Heap,一个是管理堆的策略Policy,比如对于UseSerialGC,那么创建的堆就是GenCollectedHeap,堆管理的策略就是MarkSweepPolicy;在HotSpot中,堆的实现是一种典型的分代实现,简单来说分为新生代和老年代,不同的分代存放的对象具有不一样的特征,但是不同特征的对象也可能放在一起,分在不同分代中的特征包括对象的GC年龄以及对象的大小等因素,对象将优先在Eden中存活,经过多次Minor GC依然存活的对象将晋升(Promotion)到老年代,但是晋升可能失败,所以有部分本该晋升到老年代的对象依然存活在新生代,而在做Minor GC的时候,如果Eden + From中存活的对象无法拷贝到To区域,那么也会直接转移到老年代,这称为提前晋升,还有一些比较大的对象会直接在老年代申请空间;下面的文章将以UseSerialGC为例,看看堆创建的后续流程。
先来看一下create_heap_with_policy函数的实现:
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函数应该是我们应该主要关心的,这个函数在基类GenCollectorPolicy中实现:
virtual void initialize_all() {
CollectorPolicy::initialize_all();
initialize_generations();
}
CollectorPolicy::initialize_all()函数的实现在CollectorPolicy里面,实现如下:
virtual void initialize_all() {
initialize_alignments();
initialize_flags();
initialize_size_info();
}
initialize_alignments会根据os的page大小来设置空间对齐参数,稍后会根据这些对齐参数来将我们设置的各种堆大小对齐到合理的值,所以JVM里面的实际堆大小并不会精确的等于我们设置的大小,而是会做对齐操作;
initialize_flags的工作是根据我们设定的JVM参数来设置一些全局变量的值,这里的设置是"修正"设置,在参数解析的时候已经设置过了,但是现在某些参数需要被重写设置,比如堆的大小参数,需要对齐一下大小再重新设置。比如下面的代码片段:
_min_heap_byte_size表示堆的最小值,align_size_up函数用于对齐堆的大小;aligned_initial_heap_size是对齐之后的堆初始化大小,如果和InitialHeapSize大小不一样,就要重新设置一下InitialHeapSize;MaxHeapSize也是同样的处理方法;initialize_size_info函数相对来说比较复杂,它的工作就是确定新生代和老年代的堆大小,比如新生代的初始化堆大小,以及最大堆大小等信息,下面看看细节:
// 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设置了新生代的大小,那么就不用执行这段代码,否则就要通过scale_by_NewRatio_aligned函数来确定新生代的大小,scale_by_NewRatio_aligned的实现如下:
size_t GenCollectorPolicy::scale_by_NewRatio_aligned(size_t base_size) {
return align_size_down_bounded(base_size / (NewRatio + 1), _gen_alignment);
}
我们可以使用-XX:NewRatio来设置新生代的占用整个堆的比例,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是相等的,那么就会执行这段代码,_max_young_size会根据是否设定了NewSize来确定,如果设定了那就取设定的NewSize(-Xmn),接着_initial_young_size会被设定了_max_young_size,也就是新生代不可扩展了;这里稍微说一下,DefNew不会进行堆扩展,如果Eden无法满足申请空间的要求的时候,他就会尝试去From去申请内存;如果堆可扩展,那么就会执行下面的代码:
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都已经确定了,下面就是确定老年代的这三个变量;这部分内容就不再赘述了,后续再专门研究吧。
执行完CollectorPolicy::initialize_all()之后,initialize_generations就要被执行,这个函数为创建和初始化堆进行准备,对于MarkSweepPolicy来说实现如下:
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,上面计算好的新生代老年代的堆大小也被设置到GenerationSpec对象中了,后续会使用这些参数来创建具体的堆以及初始化堆空间。
Heap初始化
下面以GenCollectedHeap为例看看堆是如何初始化的;在initialize_heap中调用create_heap之后,就会调用创建好的堆的initialize函数来初始化堆,对应着看GenCollectedHeap的initialize函数;
主要看标记出来的两行代码,分别初始化了新生代和老年代,gen_policy()->young_gen_spec()函数将返回上面设定的GenerationSpec,然后init函数将根据具体的堆类型进行创建新生代和老年代并且初始化;
比如对于+UseSerialGC,新生代就是DefNew,老年代就是MarkSweepCompact;下面看看新生代是如何进行初始化的,DefNewGeneration::DefNewGeneration这个构造函数将用来创建一个DefNew,下图展示了几个关键的地方: