[讨论] 求问JVM参数解析问题

yuyinyang 2013-03-04
最近在看OpenJDK源码碰到一个问题,我想知道当在命令行输入命令启动JVM之后,虚拟机是如何对其一系列的JVM参数进行解析的呢?
比如说这样一条启动Java程序的命令,
java -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseParallelGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 Hello.java
那么这些以字符串形式输入的参数比如UseParallelGC、PrintGCDetails等,它们在虚拟机里如何存储,如何使用?比如上面这条命令参数中指定了要使用ParallelGC收集器,那么在虚拟机里是如何一步一步地显式调用并最终使用ParallelGC收集器进行垃圾回收的?
如何可以的话希望能把OpenJDK源码中的详细调用过程指明出来。
望大牛解惑,谢谢。
RednaxelaFX 2013-03-09
呵呵呵,不是不能写但些起来很长很麻烦…

如果楼主有兴趣自己调试的话我可以简单指出几个适合挂断点的地方,然后楼主一点点单步执行过去就能看到VM初始化的全过程了。

有兴趣自己调试但是没啥经验的话,刚有人发了篇调试HotSpot VM的心得,请参考:http://hllvm.group.iteye.com/group/topic/35798。这是在Windows上的,如果在其它平台上可以看些别的教程。

适合下断点的地方,
一是java launcher里的JavaMain函数
int JNICALL JavaMain(void * args)

二是HotSpot VM里的Threads::create_vm函数
jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain)

及其所调用的Arguments::parse函数
jint Arguments::parse(const JavaVMInitArgs* args)

然后universe_init函数也可以一跟
jint universe_init()


没兴趣自己调试的话,《Java Performance》一书有简要提到java launcher和HotSpot VM的初始化过程,我也记过点笔记:http://book.douban.com/annotation/15046649/

=================================================================

简单写写吧。

HotSpot VM的VM参数(VM flags)是指那些通过-XX:前缀输入的参数。它们是以全局变量的形式存储的(这不是好习惯,乖同学请不要学习)。

-----------------------------------------------------------------

先看VM参数是如何存储的。

runtime/globals.hpp以及各组件、平台相关的*_gloabals.hpp有这些参数的声明,然后在对应的cpp文件里有它们的实现。
仔细观察那些声明VM参数的宏就可以发现,一个VM参数伴随着若干辅助结构。

例如说这样声明的一个product参数,UseParallelGC,
  product(bool, UseParallelGC, false,                                       \
          "Use the Parallel Scavenge garbage collector")                    \

在product build中,被宏展开之后实际上会变成:
// globals.hpp

// declare the VM flag
extern "C" bool UseParallelGC; // from DECLARE_PRODUCT_FLAG

// globals.cpp

// define the VM flag
bool UseParallelGC = false; // from MATERIALIZE_PRODUCT_FLAG

// keep all VM flags in a table
static Flag flagTable[] = {
  // ...
  { "bool", "UseParallelGC", &UseParallelGC, "{product}", DEFAULT }, // from RUNTIME_PRODUCT_FLAG_STRUCT
  // ...
  {0, NULL, NULL}
};

Flag* Flag::flags = flagTable;

// globals_extension.hpp

// the two enums below are used to index into the flags table
typedef enum {
 // ...
 Flag_UseParallelGC
 // ...
} CommandLineFlag;

typedef enum {
 // ...
 Flag_UseParallelGC_bool
 // ...
} CommandLineFlagWithType;


Flag结构体的数据部分如下:
struct Flag {
  const char *type;
  const char *name;
  void*       addr;
  const char *kind;
  FlagValueOrigin origin;
};


flagTable这个Flag数组记录了VM参数的名字与存储位置(地址)之间的对应关系。于是通过它就可以实现从字符串到实际全局变量的赋值,例如这样:
先遍历flagTable找到名字所指定的Flag:
// Search the flag table for a named flag
Flag* Flag::find_flag(char* name, size_t length, bool allow_locked) {
  for (Flag* current = &flagTable[0]; current->name != NULL; current++) {
    if (str_equal(current->name, name, length)) {
      // Found a matching entry.  Report locked flags only if allowed.
      if (!(current->is_unlocked() || current->is_unlocker())) {
        if (!allow_locked) {
          // disable use of locked flags, e.g. diagnostic, experimental,
          // commercial... until they are explicitly unlocked
          return NULL;
        }
      }
      return current;
    }
  }
  // Flag name is not in the flag table
  return NULL;
}

然后通过Flag里记录的地址给VM参数对应的全局变量赋值:
class Flag {
  // ...

  void set_bool(bool value)   { *((bool*) addr) = value; }

  // ...
};


有几个宏专门用于设置VM参数的值的,例如FLAG_SET_DEFAULT、FLAG_SET_CMDLINE、FLAG_SET_ERGO,顾名思义。

在VM里实际要读某个VM参数的值通常直接访问那个全局变量就行:
if (UseParallelGC) {
  // ...
}


上面只是提到product参数在product build(也就是平时我们拿到的JDK版本)的情况。product_pd、product_rw、manageable、diagnostic、experimental参数也类似。那么除此之外的参数类型(develop、notproduct等参数类型又会怎样呢?它们在非product build中跟其它参数类型一样可赋值,但在product build中会被直接声明为全局常量,例如
  develop(intx, TraceBytecodesAt, 0,                                        \
          "Traces bytecodes starting with specified bytecode number")       \

在product build中就会被宏展开为:
const intx TraceBytecodesAt = 0;

由于C++编译器在做优化的时候能有效处理这种常量的访问,所以在product build的VM里用这种VM参数没啥额外开销,而在非product build(或者说debug build)里则提供了更高的开发时灵活性。

-----------------------------------------------------------------

然后看VM参数如何从命令行的文本转换为VM内的存储形式。这主要通过java launcher与HotSpot VM里的Arguments类配合完成。

首先是java launcher。它的代码可以参考这里:
hotspot/src/share/tools/launcher
或者这里:
jdk/src/share/bin
java launcher严格说不是HotSpot VM的一部分,而属于JDK或者说JRE的组成部分。

多数JVM被实现为库(library)而不是独立的可执行程序,要启动JVM需要相应的启动程序(launcher)。java(或者在Windows上称为java.exe)是最常见的标准launcher;第三方也可以自己通过JNI提供的API(例如JNI_CreateJavaVM)来编写自定义的launcher,例如说用于将JVM嵌在自己的应用程序中。其实java launcher自己也是通过JNI_CreateJavaVM函数来创建和启动JVM的。

java launcher的build来自jdk/makefiles/CompileLaunchers.gmk

这函数长啥样呢?
typedef struct JavaVMOption {
    char *optionString;  /* the option as a string in the default platform encoding */
    void *extraInfo;
} JavaVMOption;

typedef struct JavaVMInitArgs {
    jint version;

    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);

可以看到它接受的第三个参数是指向JavaVMInitArgs结构体的指针,而这个结构体进一步指向一组JavaVMOption,每个option里有个optionString,这就是参数内容。

引用JNI规范里的范例代码,顺带添加俩HotSpot VM特有的VM参数:
JavaVMInitArgs vm_args;
JavaVMOption options[6];

options[0].optionString = "-Djava.compiler=NONE";           /* disable JIT */
options[1].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */
options[2].optionString = "-Djava.library.path=c:\mylibs";  /* set native library path */
options[3].optionString = "-verbose:jni";                   /* print JNI-related messages */
options[4].optionString = "-XX:+PrintGCDetails";            /* HotSpot specific: print detailed GC log */
options[5].optionString = "-XX:MaxPermSize=256m";           /* HotSpot specific: set maximal PermGen size */

vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 6;
vm_args.ignoreUnrecognized = TRUE;

/* Note that in the JDK/JRE, there is no longer any need to call
 * JNI_GetDefaultJavaVMInitArgs.
 */
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...

可以看到这个API接受的optionString是已经切割好的参数字符串。
以java launcher/HotSpot VM的组合为例,如何把命令行输入的一大行东西切割成符合这格式的字符串是java launcher的职责;通过JNI_CreateJavaVM函数传给JVM之后,如何使用这些参数来初始化VM就是HotSpot VM自己的职责。

可以重点关注java launcher里的ParseArguments()函数来看它是如何把长字符串切割为一个个参数,其中调用AddOption()来实际把切割好的参数字符串添加到options数组里去。
留意到java launcher所接受的参数形式是类似这样的:
java [options] <Main-Class name> [application options]

其中在<Main-Class name>之前的参数都是以减号(-)开头的。也正是这些参数会被java launcher挑出来传给JVM。

有些老形式的参数在传给JVM前会被转换为新形式的,例如:
-ms -> -Xms
-mx -> -Xmx
-ss -> -Xss
-verbosegc -> -verbose:gc

有少量特殊的参数,例如-server、-client,会被java launcher用于选择VM;这些参数就不会被传给JVM。
JVM的选择也会受java launcher里的ergonomics影响,例如如果没有强制指定-server或-client,并且java launcher觉得当前运行环境满足“ServerClassMachine”的要求就会自动选择-server。

-----------------------------------------------------------------

这些参数传到HotSpot VM里之后会交给Arguments类来处理。这边要做的最基本的事情就是按顺序把传入的参数映射到HotSpot VM实际用来存VM参数值的全局变量上;同名参数被赋值多次的话后面的值会覆盖掉前面的值。除此之外也还得做些别的事:
1、找出已经废弃(deprecate)的VM参数并输出警告,然后忽略掉这些参数
2、校验参数的合理性
3、把某些参数转换为HotSpot VM内部实际在用的参数。例如把-Xmx转换为对MaxHeapSize这个VM参数的设置。
4、根据某些参数的值设置一些关联的参数值,例如设置了-XX:+PrintGCDetails就会关联设置-XX:+PrintGC。
5、根据环境信息设置某些参数的默认值。这个功能称为ergonomics。例如说JDK6u18的release notes所描述的,没有指定-Xmx的时候默认设定Java heap大小的逻辑就在Arguments::set_heap_size():
引用
Garbage collection improvements
Updated Client JVM heap configuration
In the Client JVM, the default Java heap configuration has been modified to improve the performance of today's rich client applications. Initial and maximum heap sizes are larger and settings related to generational garbage collection are better tuned.
  • The default maximum heap size is half of the physical memory up to a physical memory size of 192 megabytes and otherwise one fourth of the physical memory up to a physical memory size of 1 gigabyte.
  • For example, if your machine has 128 megabytes of physical memory, then the maximum heap size is 64 megabytes, and greater than or equal to 1 gigabyte of physical memory results in a maximum heap size of 256 megabytes.
  • The maximum heap size is not actually used by the JVM unless your program creates enough objects to require it. A much smaller amount, termed the initial heap size, is allocated during JVM initialization. This amount is at least 8 megabytes and otherwise 1/64 of physical memory up to a physical memory size of 1 gigabyte.
  • The maximum amount of space allocated to the young generation is one third of the total heap size.
  • The updated heap configuration ergonomics apply to all collectors except Concurrent Mark-Sweep (CMS). CMS heap configuration ergonomics remain the same.
  • Server JVM heap configuration ergonomics are now the same as the Client, except that the default maximum heap size for 32-bit JVMs is 1 gigabyte, corresponding to a physical memory size of 4 gigabytes, and for 64-bit JVMs is 32 gigabytes, corresponding to a physical memory size of 128 gigabytes.


说来这里还有独立于java launcher的、HotSpot VM自己的os::is_server_class_machine()判断,由Arguments::set_ergonomics_flags()调用。HotSpot VM在server class machine上默认自动选择UseParallelGC、64位VM在小于32G的Java heap自动开启UseCompressedOops之类的功能都在这里实现。

-----------------------------------------------------------------

参数处理是HotSpot VM启动过程中的重要步骤,许多VM组件都得在那之后才可以初始化。
GC heap及对应的collector便是受参数影响很深的部分,其初始化可以从universe_init()开始看。其中它会调用Universe::initialize_heap():
jint Universe::initialize_heap() {

  if (UseParallelGC) {
#ifndef SERIALGC
    Universe::_collectedHeap = new ParallelScavengeHeap();
#else  // SERIALGC
    fatal("UseParallelGC not supported in this VM.");
#endif // SERIALGC

  } else if (UseG1GC) {
#ifndef SERIALGC
    G1CollectorPolicy* g1p = new G1CollectorPolicy();
    G1CollectedHeap* g1h = new G1CollectedHeap(g1p);
    Universe::_collectedHeap = g1h;
#else  // SERIALGC
    fatal("UseG1GC not supported in java kernel vm.");
#endif // SERIALGC

  } else {
    GenCollectorPolicy *gc_policy;

    if (UseSerialGC) {
      gc_policy = new MarkSweepPolicy();
    } else if (UseConcMarkSweepGC) {
#ifndef SERIALGC
      if (UseAdaptiveSizePolicy) {
        gc_policy = new ASConcurrentMarkSweepPolicy();
      } else {
        gc_policy = new ConcurrentMarkSweepPolicy();
      }
#else   // SERIALGC
    fatal("UseConcMarkSweepGC not supported in this VM.");
#endif // SERIALGC
    } else { // default old generation
      gc_policy = new MarkSweepPolicy();
    }

    Universe::_collectedHeap = new GenCollectedHeap(gc_policy);
  }

  jint status = Universe::heap()->initialize();
  
  // ...

  return JNI_OK;
}

这里就可以看到VM参数是如何影响GC heap的创建了。HotSpot VM里每种类型的GC heap跟收集算法是绑定在一起的,例如说选用UseParallelGC就会使用ParallelScavengeHeap作为GC heap类型,而它对应的收集器就会是:young GC:PSScavenge;full GC:PSMarkSweep。

=================================================================

HotSpot VM在做参数处理的地方有很多坑…有啥具体问题再问吧
yuyinyang 2013-03-14
这几天按着R大的指引看了参数处理这部分的源码,真是有种豁然开朗的感觉,不然真不知道怎么看,太感谢R大了。
另外还有一个小问题想问一下,我想每次只单独编译hotspot而不是编译整个OpenJDK,OpenJDK Build README里面有一段是说:
引用
Optional Import JDK
The ALT_JDK_IMPORT_PATH setting is only needed if you are not building the entire JDK. For example, if you have built the entire JDK once, and wanted to avoid repeatedly building the Hotspot VM, you could set this to the location of the previous JDK install image and the build will copy the needed files from this import area.

我不是很明白,我在已经编译好的openjdk环境下应该如何重新编译hotspot?需要重新设置哪些环境变量吗?
RednaxelaFX 2013-03-14
yuyinyang 写道
另外还有一个小问题想问一下,我想每次只单独编译hotspot而不是编译整个OpenJDK,

请参考这帖的最下面:http://rednaxelafx.iteye.com/blog/875957

我平时开发的时候就是只build出HotSpot VM而不build其它部分的。基本上在hotspot/make/<platform>目录里执行make就行。
我具体执行的命令是在hotspot/make/linux里:
make ALT_BOOTDIR=$JAVA_HOME LANG=C HOTSPOT_BUILD_JOBS=4 ARCH_DATA_MODEL=64 fastdebug

如果要build出OpenJDK 8的话请使用JDK7作为boot JDK。ALT_JDK_IMPORT_PATH这里可以不管。build出来的东西在hotspot/make/<platform>/build里找libjvm.so(和sa-jdi.jar之类的几个,替换到一个现成的JDK里就行)。

yuyinyang 写道
OpenJDK Build README里面有一段是说:
引用
Optional Import JDK
The ALT_JDK_IMPORT_PATH setting is only needed if you are not building the entire JDK. For example, if you have built the entire JDK once, and wanted to avoid repeatedly building the Hotspot VM, you could set this to the location of the previous JDK install image and the build will copy the needed files from this import area.

我不是很明白,我在已经编译好的openjdk环境下应该如何重新编译hotspot?需要重新设置哪些环境变量吗?

这段只是说如果你从OpenJDK源码的top-level开始build,它的makefile可以帮你做import啊export之类的(例如说就不用手工拷贝build出来的libjvm.so),那可以把ALT_JDK_IMPORT_PATH环境变量设到一个你之前完整build好的OpenJDK上然后指定只编译HotSpot VM。我懒,没这么用过所以也不太清楚具体要怎么操作…我都是直接无视top-level makefile直接到hotspot目录里去弄的。
yuyinyang 2013-03-14
R大我按照你说的单独编译了openjdk,我的版本是1.7,不知道是不是因为我make的是jvmg,我在hotspot/make/linux下没找到build文件夹,于是我回到openjdk的top level下查找libjvm.so
引用
[20:27 ../workspace/openjdk] find . -name libjvm.so
./hotspot/make/linux/linux_amd64_compiler2/jvmg/libjvm.so
./build/linux-amd64-debug/j2sdk-image/jre/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/hotspot/import/jre/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/libjvm.so
./build/linux-amd64-debug/j2re-image/lib/amd64/server/libjvm.so
./build/linux-amd64/j2sdk-image/jre/lib/amd64/server/libjvm.so
./build/linux-amd64/lib/amd64/server/libjvm.so
./build/linux-amd64/hotspot/import/jre/lib/amd64/server/libjvm.so
./build/linux-amd64/hotspot/outputdir/linux_amd64_compiler2/product/libjvm.so
./build/linux-amd64/j2re-image/lib/amd64/server/libjvm.so

按照你的意思,如果要使用新编译好的jvm,是不是应该将/hotspot/make/linux/linux_amd64_compiler2/jvmg/libjvm.so替换掉所有/OpenJDK/build目录下的libjvm.so?
另外你说的那个sa-jdi.jar也需要替换吗?还有其他什么需要替换的文件吗?
RednaxelaFX 2013-03-14
yuyinyang 写道
R大我按照你说的单独编译了openjdk,我的版本是1.7,不知道是不是因为我make的是jvmg,我在hotspot/make/linux下没找到build文件夹,于是我回到openjdk的top level下查找libjvm.so
引用
[20:27 ../workspace/openjdk] find . -name libjvm.so
./hotspot/make/linux/linux_amd64_compiler2/jvmg/libjvm.so
./build/linux-amd64-debug/j2sdk-image/jre/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/hotspot/import/jre/lib/amd64/server/libjvm.so
./build/linux-amd64-debug/hotspot/outputdir/linux_amd64_compiler2/jvmg/libjvm.so
./build/linux-amd64-debug/j2re-image/lib/amd64/server/libjvm.so
./build/linux-amd64/j2sdk-image/jre/lib/amd64/server/libjvm.so
./build/linux-amd64/lib/amd64/server/libjvm.so
./build/linux-amd64/hotspot/import/jre/lib/amd64/server/libjvm.so
./build/linux-amd64/hotspot/outputdir/linux_amd64_compiler2/product/libjvm.so
./build/linux-amd64/j2re-image/lib/amd64/server/libjvm.so

按照你的意思,如果要使用新编译好的jvm,是不是应该将/hotspot/make/linux/linux_amd64_compiler2/jvmg/libjvm.so替换掉所有/OpenJDK/build目录下的libjvm.so?
另外你说的那个sa-jdi.jar也需要替换吗?还有其他什么需要替换的文件吗?

嗯我之前写错了在make/linux里make的话生成的目录不是build…平时都用很久之前写的脚本,好久没手工做这种事情都开始糊涂了呵呵

我通常就替换这俩:
hotspot/make/linux/linux_amd64_compiler2/jvmg/libjvm.so -> $JDK_PATH/jre/lib/amd64/server/libjvm.so
hotspot/make/linux/linux_amd64_compiler2/generated/sa-jdi.jar -> $JDK_PATH/lib/sa-jdi.jar

其中$JDK_PATH替换为你实际想拷贝去的JDK的路径。
yuyinyang 2013-03-15
R大,我昨天在你的指点下成功单独编译了hotspot,也按照你上面说的替换了libjvm.so和sa-jdi.jar文件,然后分别运行了top level下的/build/linux-amd64/bin/java和JDK_PATH下的openjdk/build/linux-amd64/j2sdk-image/bin/java两个java程序,我用-verbose:gc打出了Java虚拟机垃圾收集结果,我发现打出的信息不一样,比如JDK_PATH下的java会多打出
引用
VM option '+PrintGCDetails'
VM option 'SurvivorRatio=8'
...

这样的一些消息,我想知道这是什么原因呢?
还有另外一个问题,我刚才修改了hotspot下的某个cpp文件然后重新编译hotspot,但是出现了错误,大致是说某个so文件已经存在无法创建,由于shell的buffer已经刷太多所以找不到那个出错的位置了,好像是libjvmxx.so.1之类的文件,于是我删除了hotspot/make/linux下的linux_amd64_compiler2目录和linux_i486_compiler2目录,再次编译的话却报了下面的错误:
引用
Linking vm...
/usr/bin/ld: /usr/lib/gcc/x86_64-redhat-linux/4.6.3/libstdc++.a(ios_init.o): relocation R_X86_64_32 against `pthread_cancel' can not be used when making a shared object; recompile
with -fPIC
/usr/lib/gcc/x86_64-redhat-linux/4.6.3/libstdc++.a: could not read symbols: Bad value
collect2: ld returned 1 exit status
/usr/bin/chcon: cannot access `libjvm.so': No such file or directory
ERROR: Cannot chcon libjvm.so
Linking launcher...
/usr/bin/ld: cannot find -ljvm
collect2: ld returned 1 exit status
make[2]: *** [gamma] Error 1
make[2]: Leaving directory `/home/yuyinyang/workspace/openjdk/hotspot/make/linux/linux_amd64_compiler2/jvmg'make[1]: *** [the_vm] Error 2
make[1]: Leaving directory `/home/yuyinyang/workspace/openjdk/hotspot/make/linux/linux_amd64_compiler2/jvmg'
make: *** [jvmg] 错误 2

为什么会出现这种情况呢,他这里要链接的libjvm.so是bootstrap jdk中的so文件吗?为什么我之前编译可以成功呢。。
RednaxelaFX 2013-03-15
因为j2sdk-image下的是product build,而你做的是jvmg build(也就是以前叫做debug build的东西)。有这个参数:
product(bool, PrintVMOptions, trueInDebug,                                \
       "Print flags that appeared on the command line")

这个的默认值是trueInDebug,也就是product build里默认是false,debug build里默认是true。这个参数为true时就会看到你说的那种输出。

=====================================

再删除一次/home/yuyinyang/workspace/openjdk/hotspot/make/linux/linux_amd64_compiler2/试试。另外其实我是不建议把ALT_BOOTDIR设置为之前自己build的OpenJDK,比较容易出问题…
yuyinyang 2013-03-15
我把ALT_BOOTDIR设置的是系统的jdk1.6的,但是还是报同样的错误。我甚至重新拷贝了一个新的openjdk1.7,在其目录下重新编译hotspot,还是报一样的错误。
我觉得很奇怪,但我之前却是是编译成功了,我之后并没有修改过任何文件,但是这个错误看来应该是它无法找到bootstrap jdk的libjvm.so文件
我想问一下,在单独编译hotspot虚拟机的时候它会去链接其他的jvm库吗?如果会的话,该文件是放在什么地方的呢?
RednaxelaFX 2013-03-16
单独编译HotSpot VM时它不会去连接bootstrap JDK里的东西。
Global site tag (gtag.js) - Google Analytics