LoginSignup
1

JVMのオプションを各場面で指定する方法(特にmaven-surefire-plugin)

Posted at

アプリケーションをJava 8から11にバージョンアップする際の作業で、Java仮想マシン(JVM)に --illegal-access=warn を設定して様子をみることがありました。JVMのオプションの設定方法が場面によって色々あったため、今後も使いそうな方法をメモしておきます。

試したバージョンは以下のとおりです。

  • (OS: Ubuntu 18.04)
  • Java: OpenJDK 11.0.18
  • Tomcat: 8.5.89
  • Maven: 3.9.1
  • maven-surefire-plugin: 3.0.0

TL;DR

今回調べてわかった主な方法です。一番上の方法に早く気付いていたら苦労しませんでした。

  • 多くの場面で動作
    • 環境変数 JDK_JAVA_OPTIONS (Java9以降)または JAVA_TOOL_OPTIONS で指定する
  • java コマンドで使う
    • コマンドライン引数で指定する ← これが基本
  • Tomcatで使う
    • 環境変数 JAVA_OPTS または CATALINA_OPTS で指定する
  • Mavenで使う
    • 環境変数 MAVEN_OPTS で指定する
    • ファイル ${maven.projectBasedir}/.mvn/jvm.config で指定する
  • 単体テストで使う(maven-surefire-plugin)
    • パラメータ argLine で指定する
      • pom.xml 内でプラグインの <configuration> に書く
      • pom.xml 内で <properties> に書く
      • mvn のコマンドライン引数で -DargLine='...' と指定する
  • JShellで使う
    • コマンドライン引数で -R'...' -R'...' と1つずつ指定する

設定方法の実験

オプションは簡単に効果を見れる -showversion で試します。

java コマンド

Javaの実行に使うコマンドです。直接使うこともあれば、スクリプト等が間接的に使っていることもあります。

ソースコードが無いと寂しいので、簡単なものを用意します。

HelloWorld.java
public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("Hello, world!");
	}
}

普通に実行すれば以下のようになります。

$ java HelloWorld.java
Hello, world!

コマンドライン引数にオプションを指定してみます。するとJavaのバージョンが表示され、オプションがきいていることがわかります。

$ java -showversion HelloWorld.java
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Hello, world!

コマンドライン引数を使わず、環境変数 JDK_JAVA_OPTIONS でも指定できます(Java9以降)。実行時には環境変数からオプションを拾ったことが注記されます。
ツール・リファレンス # JDK_JAVA_OPTIONSランチャ環境変数の使用方法

$ export JDK_JAVA_OPTIONS='-showversion'

$ java HelloWorld.java
NOTE: Picked up JDK_JAVA_OPTIONS: -showversion
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Hello, world!

Java9より前からある環境変数 JAVA_TOOL_OPTIONS も同様です。ただ、 -showversion はこの方法だと拒否されてしまいました。

$ unset JDK_JAVA_OPTIONS  # 上で試した設定を消去

$ JAVA_TOOL_OPTIONS='-showversion' java HelloWorld.java
Picked up JAVA_TOOL_OPTIONS: -showversion
Unrecognized option: -showversion
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.

$ JAVA_TOOL_OPTIONS='--illegal-access=warn' java HelloWorld.java
Picked up JAVA_TOOL_OPTIONS: --illegal-access=warn
Hello, world!

参考:javaコマンドで使える、JDK_JAVA_OPTIONS環境変数ってなんだ? - CLOVER🍀

Tomcat

Tomcatでも JDK_JAVA_OPTIONS は有効です。しかし、手元にあるアプリケーションをデプロイする際は環境変数 JAVA_OPTS でメモリ量などを指定しています。

オプションの指定方法はTomcatのスクリプトのコメントに色々書いてあります。基本的には JAVA_OPTS よりも CATALINA_OPTS のほうが良いみたいです。
https://github.com/apache/tomcat/blob/8.5.x/bin/catalina.sh

CATALINA_OPTS
(Optional) Java runtime options used when the "start", "run" or "debug" command is executed.
Include here and not in JAVA_OPTS all options, that should only be used by Tomcat itself, not by the stop process, the version command etc.
Examples are heap size, GC logging, JMX ports etc.

JAVA_OPTS
(Optional) Java runtime options used when any command is executed.
Include here and not in CATALINA_OPTS all options, that should be used by Tomcat and also by the stop process, the version command etc.
Most options should go into CATALINA_OPTS.

実際にTomcatを起動させると以下のようになります。( JDK_JAVA_OPTIONS はTomcatによって自動で追加されます)

$ export JAVA_OPTS='-showversion'

$ ./apache-tomcat-8.5.89/bin/startup.sh
Using CATALINA_BASE:   /home/user/apache-tomcat-8.5.89
Using CATALINA_HOME:   /home/user/apache-tomcat-8.5.89
Using CATALINA_TMPDIR: /home/user/apache-tomcat-8.5.89/temp
Using JRE_HOME:        /usr
Using CLASSPATH:       /home/user/apache-tomcat-8.5.89/bin/bootstrap.jar:/home/user/apache-tomcat-8.5.89/bin/tomcat-juli.jar
Using CATALINA_OPTS:   
Tomcat started.

$ less ./apache-tomcat-8.5.89/logs/catalina.out
NOTE: Picked up JDK_JAVA_OPTIONS:  --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
...

Maven

Mavenもやはり JDK_JAVA_OPTIONS は有効です。一方でTomcatで使えた JAVA_OPTS は効果が無く、また mvn コマンドの引数に java と同じように指定するとMavenのオプションとして認識されてしまいました。

$ JAVA_OPTS='-showversion' mvn -v
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
...

$ mvn -showversion
[ERROR] Error executing Maven.
[ERROR] The specified user settings file does not exist: /home/user/howversion

公式ドキュメントによると、JVMのオプションを指定するには主に以下の方法があるようです。

  • 環境変数 MAVEN_OPTS で指定する
  • ファイル ${maven.projectBasedir}/.mvn/jvm.config に書いておく
    (→ 実行スクリプト内で MAVEN_OPTS に合成される)

MAVEN_OPTS だと確かに効果があります。

$ MAVEN_OPTS='-showversion' mvn -v
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
...

単体テスト(maven-surefire-plugin)

単体テストのログを見ていたところ、 MAVEN_OPTS でオプションを有効にしてもテスト時には反映されていないことに気付きました。

単体テストに関わるmaven-surefire-pluginを調べると、forkしたJVMに MAVEN_OPTS の内容は引き継がないと書いてあります。JVMの設定はパラメータ argLine (または debugForkedProcess による追加)で行うようです。
Maven Surefire Plugin – surefire:test

<argLine>
Arbitrary JVM options to set on the command line.

<jvm>
Option to specify the jvm (or path to the java executable) to use with the forking options. For the default, the jvm will be a new instance of the same VM as the one used to run Maven. JVM settings are not inherited from MAVEN_OPTS.


設定方法でも少しハマりました。最初は pom.xml の <configuration> 内に <argLine> を書いたのですが、するとCIを回した際にJaCoCoのレポートが生成されなくなっていました。

pom.xml
...
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>3.0.0</version>
			<configuration>
				<argLine>--illegal-access=warn</argLine>
				<forkCount>1</forkCount>
				<reuseForks>false</reuseForks>
			</configuration>
		</plugin>
...

どうやらJaCoCoでもVMにオプションを設定していて、上の書き方ではそれを潰してしまっているようです。
JaCoCo fails to generate reports if maven-surefire-plugin has the argline --illegal-access=permit · Issue #964 · jacoco/jacoco · GitHub

簡単な解決策として、 <configuration> 内でなく <properties> のほうで設定することにしました。

pom.xml
...
	<properties>
		...
		<argLine>--illegal-access=warn</argLine>
	</properties>
...

プロパティは mvn コマンドの引数に -D オプションでも渡せます。またMaven 3.9.0からは環境変数 MAVEN_ARGS が使えるということで、ここに -D を入れることも一応できます。

$ mvn test -DargLine='--illegal-access=warn'

$ MAVEN_ARGS='-DargLine=--illegal-access=warn' mvn test

JShell

リフレクションの基礎を学ぶ際に使ったので、これもメモしておきます。

JShellで入力したコードの実行はリモートのプロセスで行っているようで、そちらのJVMにオプションを渡す必要があります。これは jshell コマンドのオプション -R によって指定できます。(これまでと異なり、JVMのオプション1つごとに指定します)
https://docs.oracle.com/javase/jp/11/tools/jshell.html

リモートの出力が見れないため、 -showversion--illegal-access=warn などは効果がわかりにくいです。そのため今回はメモリ使用量を制限してOutOfMemoryErrorを起こしてみます。

$ jshell -R'-Xms4M' -R'-Xmx4M'
|  Welcome to JShell -- Version 11.0.18
|  For an introduction type: /help intro

jshell> "a".repeat(1_000_000).length()
|  Exception java.lang.OutOfMemoryError: Java heap space
|        at String.repeat (String.java:3161)
|        at (#1:1)

なお、環境変数 JDK_JAVA_OPTIONS はリモートにだけ、 JAVA_TOOL_OPTIONS はシェルとリモートの両方に効果がありました。シェル側のプロセスは jshell であって java が関わらないからでしょうか。

おまけ

実際にillegal reflective accessが起きる単純なサンプルコードです。以下のissueと同じ問題に突き当たったことをきっかけに、ライブラリのソースコードから要点を抜き出しました。
Jersey 2.34 with HTTPS and PATCH method fails with error when run with JDK 16 · Issue #4825 · eclipse-ee4j/jersey · GitHub

SimpleReflection.java
public class SimpleReflection {
	public static void main(String[] args) throws Exception {
		java.net.HttpURLConnection.class.getDeclaredField("method").setAccessible(true);
	}
}

実行例は以下のとおりです。

$ java --illegal-access=debug SimpleReflection.java
WARNING: Illegal reflective access by SimpleReflection to field java.net.HttpURLConnection.method
	at SimpleReflection.main(SimpleReflection.java:3)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.execute(Main.java:404)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.run(Main.java:179)
	at jdk.compiler/com.sun.tools.javac.launcher.Main.main(Main.java:119)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1