Java 8 で動いていたアプリを JDK 17 で動かすと、起動した瞬間に終わります。
Unrecognized VM option 'UseConcMarkSweepGC'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
アプリのログは 1 行も出ません。落ちているのは JVM の起動処理で、main() はまだ呼ばれていません。 アプリのコードもクラスパスも、この時点では一度も読まれていません。
原因は -XX:+UseConcMarkSweepGC(CMS の指定)です。CMS collector 本体は JDK 14 で削除されました(JEP 363)。互換のため、JDK 14 では -XX:+UseConcMarkSweepGC というフラグだけは受理して黙殺していましたが、JDK 15 でフラグ名も消えました。JVM は知らないオプションを受け取ると、起動を拒否します。
まず起動させるだけなら、JDK 17 で有効な GC 指定に置き換えます。
java -XX:+UseG1GC -cp app.jar App
UseConcMarkSweepGC を消しても、次のフラグで同じエラーが出る
このエラーへの最短の対応は「そのフラグを消す」ですが、たいてい 1 回では終わりません。JVM は知らないオプションを 1 つ見つけた時点で止まるので、報告されるのは常に最初の 1 つです。CMS を使っていた JAVA_OPTS には、CMS を調整するフラグが並んでいます。JDK 17 でそれぞれ単体で試すと、こうなります。
| JDK 17 に渡すフラグ | 結果 |
|---|---|
-XX:+UseConcMarkSweepGC | Unrecognized VM option 'UseConcMarkSweepGC' |
-XX:+UseParNewGC | Unrecognized VM option 'UseParNewGC' |
-XX:CMSInitiatingOccupancyFraction=70 | Unrecognized VM option 'CMSInitiatingOccupancyFraction=70' |
-XX:+UseCMSInitiatingOccupancyOnly | Unrecognized VM option 'UseCMSInitiatingOccupancyOnly' |
-XX:+CMSParallelRemarkEnabled | Unrecognized VM option 'CMSParallelRemarkEnabled' |
-XX:MaxPermSize=256m | Unrecognized VM option 'MaxPermSize=256m' |
-XX:PermSize=128m | Unrecognized VM option 'PermSize=128m' |
さらに、CMS という文字列を含まないのに同じ形で落ちるものがあります。
| JDK 17 に渡すフラグ | 結果 |
|---|---|
-XX:+CMSClassUnloadingEnabled | Unrecognized VM option 'CMSClassUnloadingEnabled' |
-XX:+CMSScavengeBeforeRemark | Unrecognized VM option 'CMSScavengeBeforeRemark' |
-XX:+UseCMSCompactAtFullCollection | Unrecognized VM option 'UseCMSCompactAtFullCollection' |
-XX:+UseParallelOldGC | Unrecognized VM option 'UseParallelOldGC' |
UseParallelOldGC は CMS も ParNew も Perm も含みません。 Java 8 世代の JAVA_OPTS には CMS と同じくらい普通に載っていて、JDK 16 で削除されています。
すべて同じ形で起動を拒否します。 1 つ消して再実行、また 1 つ消して再実行、を繰り返すことになるので、最初から JAVA_OPTS(や Dockerfile、systemd の unit、Kubernetes の manifest)を開いて、CMS・ParNew・Perm・ParallelOld を含むフラグをまとめて洗い出すほうが早く終わります。
上の 2 つの表は実測した代表例であって、網羅表ではありません(CMSIncrementalMode など、ここに無い CMS 系フラグもあります)。一覧を暗記するのではなく、設定ファイルを開いて上の 4 語で洗い出すほうが確実です。
MaxPermSize は、JDK 8 の時点ですでに効いていなかった
上の表に MaxPermSize が混ざっているのは偶然ではありません。同じフラグを JDK 8 に渡すと、こうなります。
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0
APP_STARTED
警告を出して、無視して、起動します(終了コード 0)。 PermGen は Java 8 で無くなっているので、このフラグは Java 8 の時点で何もしていません。それでもアプリは動き、終了コードは 0 のままなので、設定から外す理由が生まれません。そのまま JAVA_OPTS に乗り続けて、JDK 17 で致命傷になります。
JDK が要らなくなったフラグを扱う段は 3 つあり、同じ JAVA_OPTS の中で、フラグごとに段がばらばらです。
| 段 | JVM のふるまい | 終了コード |
|---|---|---|
| 非推奨 | フラグはある。効く。警告を出す | 0 |
| 廃止 | フラグはある。受理する。効かない(Ignoring option ...) | 0 |
| 削除 | フラグを知らない(Unrecognized VM option) | 1 |
UseConcMarkSweepGC は、JDK 8 で無言 → JDK 11 で非推奨(Option UseConcMarkSweepGC was deprecated in version 9.0 ...)→ JDK 14 で廃止 → JDK 15 以降で削除、と進みました。8 から 17 へ一気に上げると、警告が出る版(11)も、無視される版(14)も踏みません。 予告なしに最後の段に着地します。この 3 段は JVM 全体の規則で、JDK 24 で Security Manager のフラグが起動を拒否する件 や JDK 21 で UseBiasedLocking が拒否される件 も同じ道筋です。
直し方:消すのではなく、置き換える
フラグを消すだけでも JDK 17 は起動します。 ただし、そのときどの GC が選ばれるかは実行環境が決めます。JVM は「2 プロセッサ以上かつ 1792 MB 以上」を server-class と見なし、満たせば G1、満たさなければ Serial を選びます(Oracle GC Tuning Guide — Ergonomics)。実測すると、境界はそのとおりに出ます。
| 実行環境 | JDK 17 が選ぶ GC |
|---|---|
| 16 CPU / 16 GB のホスト | UseG1GC = true {ergonomic} |
--cpus=2 --memory=2g | UseG1GC = true {ergonomic} |
--cpus=1 --memory=512m | UseSerialGC = true {ergonomic} |
1 CPU に絞ったジョブや、512 MB 級の小さいコンテナは Serial 側に落ちます。 停止時間を気にして CMS を選んでいたアプリが、フラグを消しただけで単一スレッドの Serial GC で動き出す、ということが起こります。しかも何のエラーも警告も出ません。逆に、2 CPU・2 GB を割り当てた Pod や一般的な CI ランナーは server-class の側なので、G1 が選ばれます。「小さいから Serial」と決め打たず、java -XX:+PrintFlagsFinal -version で自分の環境を見てください。
だから置き換えます。-XX:+UseConcMarkSweepGC は「GC を選んだ」という運用の判断であって、消せばその判断ごと消え、あとは入れ物任せになります。
java -XX:+UseG1GC -cp app.jar App # 明示的に G1 を選ぶ
どの GC に置き換えるかは、CMS を選んだ理由で決まります。 CMS は停止時間を短くするための GC でした。同じ動機なら、まず G1(停止時間と処理量の折衷)です。停止時間をさらに詰めるなら ZGC(-XX:+UseZGC)がありますが、ZGC はメモリの使い方が G1 と違うので、コンテナに割り当てた上限が小さいと当てが外れます——切り替えるなら、その環境で測ってからにしてください。処理量だけが要るなら Parallel(-XX:+UseParallelGC)もあります。
CMS の調整値(CMSInitiatingOccupancyFraction など)は移植できません——調整対象の GC そのものが無いからです。停止時間の目標を持っているなら、G1 では -XX:MaxGCPauseMillis に読み替えることになります。当プロジェクトが実測したのは「その指定で JVM が起動すること」だけで、性能は測っていません。
なお、古い GC フラグが全部落ちるわけではありません。 たとえば -XX:+PrintGCDetails は JDK 17 でも受理されます。
[warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[info][gc] Using G1
APP_STARTED
非推奨の警告を出したうえで、新しい -Xlog:gc* に読み替えて動きます(終了コード 0)。落ちるのは「削除された」フラグだけです。
フラグを消せないとき:IgnoreUnrecognizedVMOptions
共有のベースイメージや、プラットフォームが挿している環境変数が出どころで、自分では消せないことがあります。その場合、JVM に「知らないオプションは捨てて起動しろ」と指示できます。
java -XX:+IgnoreUnrecognizedVMOptions -XX:+UseConcMarkSweepGC -cp app.jar App
# → APP_STARTED(終了コード 0)
JDK 17 で実際に起動します(実測)。ただしこれは、そのフラグだけでなく、未知のオプション全部に効きます。頼っているフラグを打ち間違えても、同じように黙って捨てられます。起動を通すための一時的な足場として使い、出どころを直すまでの間に留めてください。
フラグはどこから来ているのか
コマンドラインに書いていないのに Unrecognized VM option が出るなら、環境変数から入っています。どれも「拾った」と名乗る行を出します。
| 出どころ | 出力される行 | 影響範囲(Temurin HotSpot で実測) |
|---|---|---|
JAVA_TOOL_OPTIONS | Picked up JAVA_TOOL_OPTIONS: ... | java も javac も落ちる(=Maven や Gradle がビルドの手前で死ぬ) |
_JAVA_OPTIONS | Picked up _JAVA_OPTIONS: ... | java も javac も落ちる |
JDK_JAVA_OPTIONS | NOTE: Picked up JDK_JAVA_OPTIONS: ... | java 起動子のみ。javac は通る |
JAVA_TOOL_OPTIONS に古いフラグが入っていると、アプリが起動する前にビルドツール自身が同じエラーで落ちます。「mvn test が Unrecognized VM option で落ちるが、java -version も落ちる」という状態なら、まずこの 3 つを疑ってください。他の注入経路(@argfile など)は Security Manager の記事 の「フラグはどこに隠れているか」に一覧があります。
切り分け(うまくいかないとき)
- フラグを消したのに、別の名前で同じエラーが出る:想定どおりです。JVM は最初の 1 つで止まります。
JAVA_OPTSを開いてCMS・ParNew・Perm・ParallelOldを含む行をまとめて消してください。 -XX:-UseConcMarkSweepGC(マイナス指定)にしても落ちる:JVM はフラグ名そのものを知りません。+と-の別は無関係です。Ignoring option ...; support was removed in Nと出るが起動はする:削除ではなく廃止の段です(終了コード 0)。効いてはいないので、次の JDK で削除に進む前に消してください。Unrecognized VM optionではなくUnsupported class file major versionが出る:別の機構です。そちらはフラグではなくクラスファイルの版で、ビルドツール側の問題です(Unsupported class file major version の直し方)。- アプリではなく
mvnやgradleが落ちる:JAVA_TOOL_OPTIONSか_JAVA_OPTIONSにフラグが入っています。上の表のとおり、これらはjavacを含むすべての JVM に効きます。 - GC の設定を移したが、停止時間が伸びた:CMS の調整値は G1 に移植できません。停止時間の目標は
-XX:MaxGCPauseMillisで表現し、それでも足りなければ ZGC(-XX:+UseZGC)を検討する順になります。GC を明示していないなら、まず何が選ばれているかを確かめてください(java -XX:+PrintFlagsFinal -version | grep -E "Use.*GC ")。小さいコンテナでは Serial GC になっていることがあります。
検証環境
eclipse-temurin@sha256:89dc1a6e...(JDK 17.0.19)、ネットワーク不要- 再現:
java -XX:+UseConcMarkSweepGC -cp out AppがUnrecognized VM option 'UseConcMarkSweepGC'で終了コード 1(main()に到達せず、APP_STARTEDが出ない) - 修正:
java -XX:+UseG1GC -cp out Appが終了コード 0(Java のソースは再現・修正で同一)
再現から修正までは errfix の検証ハーネスが機械的に確認しています。本文の 2 つの表の 11 フラグがすべて JDK 17 で Unrecognized VM option になること(UseParallelOldGC を含む。マイナス指定 -XX:-UseConcMarkSweepGC も同じく拒否されること)、-XX:+ExplicitGCInvokesConcurrent は JDK 17 でも受理されて起動すること、MaxPermSize が JDK 8 では ignoring option ...; support was removed in 8.0 として無視されたうえで起動していたこと、UseConcMarkSweepGC が JDK 11 で非推奨警告つきに起動すること、JDK 17 が GC を実行環境から選ぶこと(16 CPU / 16 GB と --cpus=2 --memory=2g では G1、--cpus=1 --memory=512m では Serial GC・いずれも {ergonomic})、-XX:+UseZGC と -XX:+UseG1GC -XX:MaxGCPauseMillis=100 が JDK 17 で起動すること、-XX:+PrintGCDetails が -Xlog:gc* に読み替えられて動くこと、-XX:+IgnoreUnrecognizedVMOptions を足すと起動すること、JAVA_TOOL_OPTIONS と _JAVA_OPTIONS では javac 自身が落ち JDK_JAVA_OPTIONS では落ちないことは、いずれも Eclipse Temurin の JDK 8・11・17・21 で実測しています。
CMS collector が JDK 14 で削除された(JEP 363)こと、server-class の判定条件(2 プロセッサ以上かつ 1792 MB 以上)、UseParallelOldGC が JDK 16 で削除されたことは、OpenJDK / Oracle の一次情報に拠ります。 フラグが JDK 14 で廃止(Ignoring option)され JDK 15 で削除(Unrecognized VM option)に進んだ点は、Temurin に 12〜15 のイメージが無いため AdoptOpenJDK のイメージで測定しました(記録は docs/probes/jvm-legacy-flags-20260713.md)。GC の性能特性は実測していません。