※当サイトの記事には、広告・プロモーションが含まれます。

javaコマンド実行時のオプション-Xmxの設定が効いているのか確認してみた

nazology.net

⇧ 宇宙に移住する日が来ますかな...

javaコマンド実行時のオプション-Xmxとは

Oracleのドキュメントによりますと、

docs.oracle.com

-Xmxsize

モリー割当てプールの最大サイズ(バイト単位)を指定します。この値は、1024の倍数で、2Mバイトより大きくなければなりません。キロバイトを指定するには文字kまたはK、メガバイトを指定するには文字mまたはM、ギガバイトを指定するには文字gまたはGを付けます。デフォルト値は、実行時にシステムの設定に基づいて選択されます。サーバー配備の場合、-Xms-Xmxが同一の値に設定されていることがよくあります。Java SE HotSpot Virtual Machineガベージ・コレクション・チューニング・ガイド』(http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html)のエルゴノミクスに関する項を参照してください。

https://docs.oracle.com/javase/jp/8/docs/technotes/tools/unix/java.html

⇧ とのこと。「-Xmxsize」って...「-Xmx」じゃないんか、と言うか紛らわしい記載を止めて欲しい...

javaコマンド実行時のオプション-Xmxの設定が効いているのか確認してみた

で、どうも、-Xmxオプションで設定したサイズで「Java仮想マシンJVMJava Virtual Machine )」を起動して、Javaのプログラムが実行できていないんじゃないかという話が現場で出てきたので、確認する方法が無いのか調べてたところ、

qiita.com

⇧ 上記サイト様で「-XX:+PrintFlagsFinal」というオプションの存在を知ったのですが、何故か、javaコマンドのドキュメントでは出て来なくて、

docs.oracle.com

ヒープ・サイズの初期値と最大値の指定

フラグ-Xms (初期ヒープ・サイズ)および-Xmx (最大ヒープ・サイズ)を使用して、初期および最大のヒープ・サイズを指定できます。アプリケーションがうまく動作するために必要なヒープ領域がわかっている場合は、-Xms-Xmxを同じ値に設定できます。そうでない場合は、JVMが初期ヒープ・サイズを使用して開始され、ヒープ使用量とパフォーマンスのバランスが取れるまで、Javaヒープが増加します。

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/vm/gctuning/parallel.html

これらのデフォルト値は、他のパラメータおよびオプションの影響を受ける場合があります。デフォルト値を確認するには、-XX:+PrintFlagsFinalオプションを使用し、出力からMaxHeapSizeを探します。たとえば、LinuxまたはSolarisでは、次を実行できます。

java -XX:+PrintFlagsFinal <GC options> -version | grep MaxHeapSize

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/vm/gctuning/parallel.html

⇧ 上記のドキュメントに記載があったのだけど、「-XX:+PrintFlagsFinal」が何なのかについての説明は無いという...流石はOracle、安定の不親切さ...

デフォルト値を確認とあるけど、現在の値の確認ということになるかと思う、たぶん...。

というわけで、「-XX:+PrintFlagsFinal」を利用して、Xmxオプションが効いているのか確認してみました。

環境は、「WSL 2(Windows Subsystem for Linux 2)」のUbuntuで試しました。

用意しているファイルは以下で、

csvディレクトリ配下には、CSVファイルが1つ配置されています。

CSVファイルは、

www.geospatial.jp

⇧ 上記サイト様で公開されているものをダウンロードしてきて利用しています。

Javaファイルの中身は以下のような感じです。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class TestJvmOption {

	public static void main(String[] args) {
		
        // 【start】処理開始                
        long start = System.currentTimeMillis();
        System.out.println("【start】処理開始:" + convertEpochTimeToLocalDateTime(start)); 
		
        Path file = Paths.get(args[0]);
        //Path file = Paths.get("csv/13tokyo500m.csv");
		System.out.println(args.length);
		//System.out.println(args[0]);		
		Map<Integer, List<String>> meshMap = new HashMap<>();

		if (Files.exists(file)) {
			int rowCount = 0;
			try (FileReader fileReader = new FileReader(file.toString());
					BufferedReader bufferedReader = new BufferedReader(fileReader)) {
				String line;
				while ((line = bufferedReader.readLine()) != null) {
					String[] arrayStr = line.split(",");
					
					meshMap.put(rowCount, Arrays.asList(arrayStr));
					rowCount++;
				}
			} catch (IOException e) {
				System.out.println(e);
			}

		}
//		meshMap.entrySet().stream()
//		  .map(entry -> String.join(",", String.valueOf(entry.getKey()), String.join(",", entry.getValue().toArray(new String[entry.getValue().size()]))))
//		  .forEach(System.out::println);
		
		List<String> beforeList = null;
		String firstCol = "";
		String strResultJoin = null;
		for (Map.Entry<Integer, List<String>> entry:  meshMap.entrySet()) {
			// 1列目が空ではない場合
			
			if (Objects.isNull(beforeList) 
					||  entry.getValue().get(0).length() !=0) {
				firstCol = entry.getValue().get(0);
				strResultJoin = joinString(String.valueOf(entry.getKey()), entry.getValue());
			} else {
				strResultJoin = joinStringWithAddFirstCol(String.valueOf(entry.getKey()), entry.getValue(), firstCol);
			}
			beforeList = entry.getValue();
			System.out.println(strResultJoin);
		}
        // 【finish】処理終了
        long end = System.currentTimeMillis();
        System.out.println("【finish】処理終了:" + convertEpochTimeToLocalDateTime(end));
        System.out.println("【処理時間】" + (end - start)  + "ms");

	}
	
	private static String joinString(String mapKey, List<String> mapValue) {
		return String.join(",", String.valueOf(mapKey), String.join(",", mapValue.toArray(new String[mapValue.size()])));
		
	}

	private static String joinStringWithAddFirstCol(String mapKey, List<String> mapValue, String firstCol) {
		String joinMapValue = String.join(",", mapValue.toArray(new String[mapValue.size()])).substring(1);
		return String.join(",", String.valueOf(mapKey), firstCol, joinMapValue);
		
	}
	
	private static String convertEpochTimeToLocalDateTime(long epochTime) {
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS");

		Instant instant = Instant.ofEpochMilli(epochTime);

        ZoneId zoneId = ZoneId.systemDefault(); // Use the system default time zone
        LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
        String formattedDateTime = localDateTime.format(formatter);
		return formattedDateTime;
	}
	
}
    

⇧ 上記は、以下のコマンドでコンパイルして、「.class」ファイルを生成しておきます。

javac TestJvmOption.java

で、シェルスクリプトの中身は以下のようになってます。

#!/bin/bash

## ■■■■■■■■■■■■■■■
## ■ 変数定義
## ■■■■■■■■■■■■■■■
jstat_TestJvmOption_tsv="jstat.tsv"
#jvm_start_option_Xmx1024m="-Xmx1024m"
jvm_start_option_Xmx2048m="-Xmx2048m"
jvm_start_option_PrintFlagsFinal="-XX:+PrintFlagsFinal"
interval_millsecond=100
limit_display_result_row_count=20000
datetime_format="%Y/%m/%d %H:%M:%S.%3N"
jvm_java_version="-version"
result_log="result.log"

## ■■■■■■■■■■■■■■■
## ■ ファイル初期化
## ■■■■■■■■■■■■■■■
if [ -e ${jstat_TestJvmOption_tsv} ]; then
  rm ${jstat_TestJvmOption_tsv} && touch ${jstat_TestJvmOption_tsv}
else
  touch ${jstat_TestJvmOption_tsv}
fi

## ■■■■■■■■■■■■■■■
## ■ Java実行
## ■■■■■■■■■■■■■■■
# JVMのバージョンを確認
#execute_java_version=$(java ${jvm_java_version})
java ${jvm_java_version} 2>> ${result_log} &&

# JVM起動時の設定を確認(Xmxの設定のみ表示)
jvm_setting_output=$(java ${jvm_start_option_Xmx2048m} ${jvm_start_option_PrintFlagsFinal} 2>&1 | grep -Ei "MaxHeapSize")
echo "${jvm_setting_output}" >> ${result_log} &&

# TestJvmOptionクラスの実行
java ${jvm_start_option_Xmx2048m} TestJvmOption $1 >> ${result_log} & 
#jps -lm | grep TestJvmOption | awk '{print $1}' | xargs -I{} jstat -gcutil {} ${interval_millsecond} ${limit_display_result_row_count} | awk -F, '{print strftime(`${datetime_format}`,systime()) $1}' >> ${jstat_TestJvmOption_tsv}
#jps -lm | grep TestJvmOption | awk '{print $1}' | xargs -I{} jstat -gc {} ${interval_millsecond} ${limit_display_result_row_count} | awk -F, '{print strftime("%(${datetime_format})",systime()) $1}' >> ${jstat_TestJvmOption_tsv}
#jps -lm | grep TestJvmOption | awk '{print $1}' | xargs -I{} jstat -gc {} ${interval_millsecond} ${limit_display_result_row_count} | awk -v datetime_format="${datetime_format}" -F, '{print strftime(datetime_format, systime()) $1}' >> ${jstat_TestJvmOption_tsv}
# jstatコマンドの出力をファイルに追記(ヘッダー行を含む)
#jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') ${interval_millsecond} ${limit_display_result_row_count} |
jps | grep TestJvmOption | awk '{print $1}' | xargs -I{} jstat -gc {} ${interval_millsecond} ${limit_display_result_row_count} | 
awk -v datetime_format="${datetime_format}" 'BEGIN { OFS="\t" } {
  split($1, arr, " ");
  ms = sprintf("%.3f", arr[1] / 1000); # ミリ秒を秒に変換し、小数点以下3桁までの文字列にフォーマット
  cmd = "date +\"" datetime_format "\""; # 日時フォーマットを含むdateコマンドを生成
  cmd | getline formatted_date; # dateコマンドを実行してフォーマットされた日時を取得
  close(cmd);

  # 各列をタブ区切りで結合
  output = formatted_date "." ms;
  for (i = 1; i <= NF; i++) {
    output = output "\t" $i;
  }
  print output;
}' >> ${jstat_TestJvmOption_tsv}

#java ${jvm_start_option_PrintFlagsFinal} ${jvm_start_option_Xmx} TestJvmOption $1 >> result.log &
#while jps | grep -q TestJvmOption; 
#do 
#  jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') 100 20000 >> jstat.log; 
#done

#until jps | grep TestJvmOption
#  do 
#    #echo "start jstat TestJvmOption"
#    jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') 100 20000 >> ${jstat_TestJvmOption_tsv}
#  done

#until jps | grep -q TestJvmOption
#  do 
#    jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') 100 20000 | awk -F, '{print strftime("%Y/%m/%d %H:%M:%S",systime()) $1}' >> ${jstat_TestJvmOption_tsv}
#  done

#until jps | grep -q TestJvmOption
#  do
#    jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') 100 20000 | perl -wn -e '$d = `date "+%Y/%m/%d %H:%M:%S"`; chomp $d; print "$d | $_"' >> ${jstat_TestJvmOption_tsv}
#  done


#jstat -gc $(jps | grep TestJvmOption | awk '{print $1}') 100 20000 | awk -F, '{print strftime("%Y/%m/%d %H:%M:%S",systime()) $1}';

で、CSVファイルのパスを引数に、シェルスクリプトのファイルを実行。

ログを抜粋すると、

openjdk version "11.0.20.1" 2023-08-24
OpenJDK Runtime Environment (build 11.0.20.1+1-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.20.1+1-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)
   size_t MaxHeapSize                              = 2147483648                                {product} {command line}
   size_t ShenandoahSoftMaxHeapSize                = 0                                      {manageable} {default}
【start】処理開始:2023/09/30 13:11:36.328
1
0,Code,Long,Lat,Flag
1,533945091,139.7375,35.6666666666667,1
2,533945091,139.74375,35.6666666666667,0
3,533945091,139.74375,35.6708333333333,0
4,533945091,139.7375,35.6708333333333,0
5,533945091,139.7375,35.6666666666667,0

...省略

52597,414241604,142.1375,27.7208333333333,0
52598,414241604,142.1375,27.725,0
52599,414241604,142.13125,27.725,0
52600,414241604,142.13125,27.7208333333333,0
【finish】処理終了:2023/09/30 13:11:37.701
【処理時間】1373ms

⇧ -Xmxオプションで設定したサイズでJVMの起動が実行されてそうです。

jstatの結果も

2023/09/30 13:11:37.481.0.000	S1C	S0U	S1U	EC	EU	OC	OU	MC	MU	CCSC	CCSU	YGC	YGCT	FGC	FGCT	CGC	CGCT	GCT
2023/09/30 13:11:37.510.0.000	2048.0	0.0	2048.0	13312.0	11264.0	50176.0	16401.5	4864.0	736.9	512.0	63.8	2	0.072	0	0.000	0	0.000	0.072
2023/09/30 13:11:37.612.0.000	1024.0	0.0	1024.0	15360.0	6144.0	49152.0	17800.7	4864.0	736.9	512.0	63.8	3	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:37.716.0.000	1024.0	0.0	1024.0	15360.0	12288.0	49152.0	17800.7	4864.0	736.9	512.0	63.8	3	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:37.818.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:37.920.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:38.021.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:38.124.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:38.225.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:38.328.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080
2023/09/30 13:11:38.429.0.000	1024.0	0.0	1024.0	18432.0	1024.0	46080.0	17656.2	4864.0	736.9	512.0	63.8	4	0.080	0	0.000	0	0.000	0.080

⇧ 取得できていそうではありますと。

で、

qiita.com

monogamieru.com

⇧ 上記サイト様の説明にあるように、jstatで計測された「確保済み領域」は、「-Xmx」で設定した値になるわけではないので、「-Xmx」で設定した値に対する割合を算出したい場合は、

Java仮想マシン(JVM:Java Virtual Machine )のメモリの使用率(予測) = (ヒープの利用領域:[S0U] + [S1U] + [EU] + [OU]) ÷ (「-Xmx」で設定した値)   

⇧ ってことになるんかね?

でも、「-Xmx」で指定した値のほとんどが使われないとしたら、「-Xmx」を指定する意味が無い気がするんだが...

結局、「-Xmx」にどのぐらいの値を設定するべきかどうか、何を基準にすれば良いか分からんでは無いか...

Microsoftのドキュメントだと、

learn.microsoft.com

⇧ アプリケーションが利用するメモリの70%ほどを「-Xmx」の値に設定する例があるので、マシンのメモリから逆算する感じになるんかね?

Dockerのようなコンテナ上で稼働してるアプリケーションなら、コンテナの上限メモリからアプリケーションに割り当てられそうな上限のメモリを逆算して、「-Xmx」の値を算出する感じか?

2023年10月4日(水)追記:↓ ここから

どうやら、

n-agetsuma.hatenablog.com

Oracleの公式ドキュメントには、-Xmxが未指定であった場合のエルゴノミクスによる最大ヒープサイズは『32GBを上限として、物理メモリの4分の1』書かれている。32GBは-XX:-UseCompressedOopsにより圧縮Oopを明示的に無効にした場合の最大デフォルトヒープサイズで、何もオプションを付けずに起動した場合は29GBが上限。

JDK8(Linux 64bit)のデフォルトヒープサイズ - n-agetsumaの日記

⇧ とのこと。

docs.oracle.com

サーバーJVMのデフォルト・ヒープ・サイズの初期値と最大値

サーバーJVM上のデフォルト・ヒープ・サイズの初期値と最大値は、より高いデフォルト値が可能であることを除き、クライアントJVMの場合と同様です。32ビットJVMのデフォルトの最大ヒープ・サイズは、4GB以上の物理メモリーがある場合、最大で1GBです。64ビットJVMのデフォルトの最大ヒープ・サイズは、128GB以上の物理メモリーがある場合、最大で32GBです。これらの値は、いつでも直接に指定して、より高い(または低い)初期および最大ヒープを設定できます。

https://docs.oracle.com/javase/jp/8/docs/technotes/guides/vm/gctuning/parallel.html#default_heap_size

⇧ う~む、最大32GBと言いつつ、より高い値を設定できると言っているように見えるんだが、マシンの搭載してる最大のメモリ量にも依るとは思うけど、結局、JVMのヒープメモリに32GB以上の値を設定できるってことなんだろうか?

2023年10月4日(水)追記:↑ ここまで

う~む、とりあえず、Oracleさんは、Javaのドキュメントもう少ししっかり整備して欲しい...

まぁ、

japan.zdnet.com

⇧ 頑張ってるのは分かるけど...

2023年10月2日(月)追記:↓ ここから

悲報...

www.infoworld.com

I haven't seen any formal explanation regarding these options (Zahid refers to one of them being "hidden away in the JVM source code" - an advantage of open source!). From the output generated, however, it is possible to gain a good idea of what is displayed. The output of running java -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions -version is text with a header row stating [Global flags] followed by numerous rows with one option and its metdata per row. Each row of the output represents a particular global option and has four columns.

https://www.infoworld.com/article/2073676/hotspot-jvm-options-displayed---xx--printflagsinitial-and--xx--printflagsfinal.html

⇧ 2011年時点で、正式なドキュメントが無いって仰っているようで、今が2023年なので彼是10年以上、ドキュメントが無い状態が続いているらしい...Oracleさん、もう用意する気が無さそうね...

と言うか、『"hidden away in the JVM source code"』って、う~む、ソースコードを追えってことかね...不親切過ぎるんだが...

ソースコードから探すには、

www.baeldung.com

Sometimes, no amount of documentation can beat the source code. Therefore, if we have the name of a particular flag, then we can explore the JVM source code to find out what’s going on.

https://www.baeldung.com/jvm-tuning-flags

For instance, we can check out the HotSpot JVM’s source code from GitHub or even their Mercurial repository and then:

>> git clone git@github.com:openjdk/jdk14u.git openjdk
>> cd openjdk/src/hotspot
>> grep -FR 'PrintFlagsFinal' .
./share/runtime/globals.hpp:  product(bool, PrintFlagsFinal, false,                                   
./share/runtime/init.cpp:  if (PrintFlagsFinal || PrintFlagsRanges) {

https://www.baeldung.com/jvm-tuning-flags

⇧ git cloneしてきて、ローカル上にソースコードを持ってきて、grep検索するのが良いようです。

ブラウザだと、検索し辛い...

github.com

ちなみに、拡張子が「.cpp」「.hpp」のファイルは、

malibu-bulldog.hatenadiary.org

⇧ 上記サイト様によりますと、C++のファイルらしいので、最早、Javaで無いというね...

2023年10月2日(月)追記:↑ ここまで

あと、相変わらず、シェルスクリプトの書き方はカオスだと感じてしまいますな...

毎度モヤモヤ感が半端ない...

今回はこのへんで。