低スペックマシンでJava 11を動かすと、デフォルトのGCはG1GCじゃなくてSerialGCになる

Java 9からG1GCがデフォルトになっていますが、低スペックマシンでJava 11を動かしたらデフォルトのGCはG1GCじゃなくてSerialGCになってました。という話です。

ちなみに、G1GCをデフォルトにする件のJEPは、JEP 248です。



今回使用したバージョン

  • Java: OpenJDK (from Oracle) 11.0.1 (build 11.0.1+13)



動作確認

PrintFlagsFinalしてみると、以下のような感じです。

デフォルトに従って決定されたGCがergonomicとして表示されています。
JVMのコマンドラインオプションで指定するとcommand lineと表示されますね。

低スペックマシンの場合

$ java -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal 2>/dev/null | egrep "Use.*GC "
     bool UseAdaptiveSizePolicyWithSystemGC  = false      {product} {default}
     bool UseConcMarkSweepGC                 = false      {product} {default}
     bool UseEpsilonGC                       = false {experimental} {default}
     bool UseG1GC                            = false      {product} {default}
     bool UseMaximumCompactionOnSystemGC     = true       {product} {default}
     bool UseParallelGC                      = false      {product} {default}
     bool UseParallelOldGC                   = false      {product} {default}
     bool UseSerialGC                        = true       {product} {ergonomic}
     bool UseZGC                             = false {experimental} {default}

低スペックマシン”ではない”場合

$ java -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal 2>/dev/null | egrep "Use.*GC "
     bool UseAdaptiveSizePolicyWithSystemGC  = false      {product} {default}
     bool UseConcMarkSweepGC                 = false      {product} {default}
     bool UseEpsilonGC                       = false {experimental} {default}
     bool UseG1GC                            = true       {product} {ergonomic}
     bool UseMaximumCompactionOnSystemGC     = true       {product} {default}
     bool UseParallelGC                      = false      {product} {default}
     bool UseParallelOldGC                   = false      {product} {default}
     bool UseSerialGC                        = false      {product} {default}
     bool UseZGC                             = false {experimental} {default}

-XX:+UseG1GCを指定した場合

$ java -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal 2>/dev/null | egrep "Use.*GC "
     bool UseAdaptiveSizePolicyWithSystemGC  = false      {product} {default}
     bool UseConcMarkSweepGC                 = false      {product} {default}
     bool UseEpsilonGC                       = false {experimental} {default}
     bool UseG1GC                            = true       {product} {command line}
     bool UseMaximumCompactionOnSystemGC     = true       {product} {default}
     bool UseParallelGC                      = false      {product} {default}
     bool UseParallelOldGC                   = false      {product} {default}
     bool UseSerialGC                        = false      {product} {default}
     bool UseZGC                             = false {experimental} {default}



ソース

Java 11でのソースはこうなってます。

void GCConfig::select_gc_ergonomically() {
  if (os::is_server_class_machine()) {
#if INCLUDE_G1GC
    FLAG_SET_ERGO_IF_DEFAULT(bool, UseG1GC, true);
#elif INCLUDE_PARALLELGC
    FLAG_SET_ERGO_IF_DEFAULT(bool, UseParallelGC, true);
#elif INCLUDE_SERIALGC
    FLAG_SET_ERGO_IF_DEFAULT(bool, UseSerialGC, true);
#endif
  } else {
#if INCLUDE_SERIALGC
    FLAG_SET_ERGO_IF_DEFAULT(bool, UseSerialGC, true);
#endif
  }
}
// This is the working definition of a server class machine:
// >= 2 physical CPU's and >=2GB of memory, with some fuzz
// because the graphics memory (?) sometimes masks physical memory.
// If you want to change the definition of a server class machine
// on some OS or platform, e.g., >=4GB on Windows platforms,
// then you'll have to parameterize this method based on that state,
// as was done for logical processors here, or replicate and
// specialize this method for each platform.  (Or fix os to have
// some inheritance structure and use subclassing.  Sigh.)
// If you want some platform to always or never behave as a server
// class machine, change the setting of AlwaysActAsServerClassMachine
// and NeverActAsServerClassMachine in globals*.hpp.
bool os::is_server_class_machine() {
  // First check for the early returns
  if (NeverActAsServerClassMachine) {
    return false;
  }
  if (AlwaysActAsServerClassMachine) {
    return true;
  }
  // Then actually look at the machine
  bool         result            = false;
  const unsigned int    server_processors = 2;
  const julong server_memory     = 2UL * G;
  // We seem not to get our full complement of memory.
  //     We allow some part (1/8?) of the memory to be "missing",
  //     based on the sizes of DIMMs, and maybe graphics cards.
  const julong missing_memory   = 256UL * M;

  /* Is this a server class machine? */
  if ((os::active_processor_count() >= (int)server_processors) &&
      (os::physical_memory() >= (server_memory - missing_memory))) {
    const unsigned int logical_processors =
      VM_Version::logical_processors_per_package();
    if (logical_processors > 1) {
      const unsigned int physical_packages =
        os::active_processor_count() / logical_processors;
      if (physical_packages >= server_processors) {
        result = true;
      }
    } else {
      result = true;
    }
  }
  return result;
}



公式ドキュメント

公式ドキュメントを探すと、ちゃんと書いてありました。
Java 11の日本語ドキュメントはまだ無さそうなので、Java 10のやつを合わせて貼っておきます。

The serial collector is usually adequate for most small applications, in particular those requiring heaps of up to approximately 100 megabytes on modern processors. The other collectors have additional overhead or complexity, which is the price for specialized behavior. If the application does not need the specialized behavior of an alternate collector, use the serial collector. One situation where the serial collector isn’t expected to be the best choice is a large, heavily threaded application that runs on a machine with a large amount of memory and two or more processors. When applications are run on such server-class machines, the Garbage-First (G1) collector is selected by default

シリアル・コレクタは一般に、特に最新のプロセッサ上で、必要なヒープが最大でも100MB程度の小規模アプリケーションのほとんどに適しています。 他のコレクタは、特殊な動作の代償として、オーバーヘッドや複雑さが増加します。 他のコレクタが備える特殊な動作をアプリケーションが必要としない場合には、シリアル・コレクタを使用します。 シリアル・コレクタが最適な選択肢にならないのは、大容量のメモリーと2個以上のプロセッサを搭載したマシンで動作する、スレッド化の程度が非常に高い大規模なアプリケーションの場合です。 アプリケーションがserver-classマシンで実行されている場合、ガベージファースト(G1)・コレクタがデフォルトで選択されます。



Java 8の場合

Java 8では、Parallel/Serialの切り替えがあると教えてもらったので、ついでにソース読んでみました。
僕はCMS以外ほとんど使った事なかったので知りませんでした。。。sugarlifeさん、ありがとうございます!

void Arguments::select_gc_ergonomically() {
  if (os::is_server_class_machine()) {
    if (should_auto_select_low_pause_collector()) {
      FLAG_SET_ERGO(bool, UseConcMarkSweepGC, true);
    } else {
      FLAG_SET_ERGO(bool, UseParallelGC, true);
    }
  }
}
bool Arguments::should_auto_select_low_pause_collector() {
  if (UseAutoGCSelectPolicy &&
      !FLAG_IS_DEFAULT(MaxGCPauseMillis) &&
      (MaxGCPauseMillis <= AutoGCSelectPauseMillis)) {
    if (PrintGCDetails) {
      // Cannot use gclog_or_tty yet.
      tty->print_cr("Automatic selection of the low pause collector"
       " based on pause goal of %d (ms)", (int) MaxGCPauseMillis);
    }
    return true;
  }
  return false;
}
// This is the working definition of a server class machine:
// >= 2 physical CPU's and >=2GB of memory, with some fuzz
// because the graphics memory (?) sometimes masks physical memory.
// If you want to change the definition of a server class machine
// on some OS or platform, e.g., >=4GB on Windohs platforms,
// then you'll have to parameterize this method based on that state,
// as was done for logical processors here, or replicate and
// specialize this method for each platform.  (Or fix os to have
// some inheritance structure and use subclassing.  Sigh.)
// If you want some platform to always or never behave as a server
// class machine, change the setting of AlwaysActAsServerClassMachine
// and NeverActAsServerClassMachine in globals*.hpp.
bool os::is_server_class_machine() {
  // First check for the early returns
  if (NeverActAsServerClassMachine) {
    return false;
  }
  if (AlwaysActAsServerClassMachine) {
    return true;
  }
  // Then actually look at the machine
  bool         result            = false;
  const unsigned int    server_processors = 2;
  const julong server_memory     = 2UL * G;
  // We seem not to get our full complement of memory.
  //     We allow some part (1/8?) of the memory to be "missing",
  //     based on the sizes of DIMMs, and maybe graphics cards.
  const julong missing_memory   = 256UL * M;

  /* Is this a server class machine? */
  if ((os::active_processor_count() >= (int)server_processors) &&
      (os::physical_memory() >= (server_memory - missing_memory))) {
    const unsigned int logical_processors =
      VM_Version::logical_processors_per_package();
    if (logical_processors > 1) {
      const unsigned int physical_packages =
        os::active_processor_count() / logical_processors;
      if (physical_packages > server_processors) {
        result = true;
      }
    } else {
      result = true;
    }
  }
  return result;
}

ぱっと見ると、is_server_class_machine関数は、Java 8とJava 11で同じっぽいですが、よーく見ると。

  • Java 11
if (physical_packages >= server_processors) {
  • Java 8
if (physical_packages > server_processors) {

Java 8はCPUが3個以上になってますね。ソースコメントは2個以上になってるんですが。。。
ドキュメントでも2個以上ってなってます。



まとめ

Java 11のデフォルトGCはG1GCですが、低スペックマシンの場合SerialGCがデフォルトになります。
低スペックマシンでテストする時に、あれ?ってならないように注意してください。
冗長ですが、-XX:+UseG1GCフラグは常に付けても良いかもしれません。

 
comments powered by Disqus