アプリケーションを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='...'
と指定する
- pom.xml 内でプラグインの
- パラメータ
- JShellで使う
- コマンドライン引数で
-R'...' -R'...'
と1つずつ指定する
- コマンドライン引数で
設定方法の実験
オプションは簡単に効果を見れる -showversion
で試します。
java
コマンド
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のレポートが生成されなくなっていました。
...
<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>
のほうで設定することにしました。
...
<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
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)