GC part 6

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这样的参数是怎么解析的。

image

-Xms用于设置堆的最小容量,-Xmx(或者-XX:MaxHeapSize)用于设置堆的最大容量,如果-Xms设置的大小和-Xmx一样大,那么堆就是不可扩展的,否则堆就是可以动态扩展的;上面的代码片段就是用来解析-Xms和-Xmx两个参数的,解析好的参数会设定到相应的全局共享变量中去,比如-Xms就会被设置到InitialHeapSize中去,-Xmx会设置到MaxHeapSize中去;这两个是数值型的参数,下面来看一个flag类型的参数设置,比如我们使用-XX:+UseConcMarkSweepGC,那么这个参数是怎么被JVM识别出来的呢?下面来分析一下。

image

还是在同样的函数里面解析,上面的代码片段是解析类似-XX:+UseConcMarkSweepGC参数的入口,parse_argument负责具体的解析工作,下面来看看parse_argument函数是怎么实现解析这样的参数并且设置到全局变量中去的。parse_argument函数可以在process_argument中找到;

image

图中标注的就是解析的关键,+%或者-%用于匹配-XX:+UseSerialGC中的+,下面可以看一个实际的启动时参数解析例子:

image

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函数来设置全局变量:

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;
}

在设置了JVM参数之后,我们也不知道参数这样设置是否存在问题,或者是否有冲突,但是JVM必须能够发现这种冲突,并且及时给出提示,check_vm_args_consistency函数将完成JVM参数设置校验的工作,比如校验GC设置是否合理是通过调用check_gc_consistency函数来完成的:

image

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函数将完成创建堆的工作;

image

创建什么类型的堆依赖于选择了什么类型的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参数来设置一些全局变量的值,这里的设置是"修正"设置,在参数解析的时候已经设置过了,但是现在某些参数需要被重写设置,比如堆的大小参数,需要对齐一下大小再重新设置。比如下面的代码片段:

image

_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函数;

image

主要看标记出来的两行代码,分别初始化了新生代和老年代,gen_policy()->young_gen_spec()函数将返回上面设定的GenerationSpec,然后init函数将根据具体的堆类型进行创建新生代和老年代并且初始化;

image

比如对于+UseSerialGC,新生代就是DefNew,老年代就是MarkSweepCompact;下面看看新生代是如何进行初始化的,DefNewGeneration::DefNewGeneration这个构造函数将用来创建一个DefNew,下图展示了几个关键的地方:

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容