JavaFXアプリケーションのプロセスが終了せずに残ってしまう

JavaFX アプリケーションに二重起動を防止する仕組みを追加したところ プロセスが終了せずに残ってしまうようになってしまいました。

これは Windows 環境の Java 11 + OpenJFX 11 で発生した事象です。他のプラットフォーム OpenJFX のバージョンによっては問題が起こらない可能性もあります。

アプリケーションは以下のような構造です。main メソッドで二重起動かどうかを判定して すでに同じアプリケーションが起動している場合は何もせずに return するようになっています。

MyApp.java
import javafx.application.Application; import javafx.stage.Stage; public class MyApp extends Application { public static void main(String[] args) { if(二重起動をチェック) { //同じアプリケーションが起動している場合は何もせずプロセスを終了する return; } launch(args); } @Override public void start(Stage stage) throws Exception { } }

このとき 後から起動したプロセスは Application.launch メソッドも呼ばないし Application の実装クラスのインスタンスも生成しません。本当に私は何もしていません。にもかかわらず 後から起動したプロセスは main メソッドを終了した後も残り続けてしまうのでした。

はじめは二重起動のチェック処理に副作用があるのではないかと疑いました。非デーモンスレッドを起動しているんじゃないか?と。

しかし 結果は違いました。原因を絞り込もうとコードを小さくしていった結果 以下のコードでもプロセスが残留してしまうことが分かりました。

MyApp.java
import javafx.application.Application; import javafx.stage.Stage; public class MyApp extends Application { public static void main(String[] args) { System.out.println("Hello, World!!"); } @Override public void start(Stage stage) throws Exception { } }

ただ文字列を出力するだけのプログラムです。こんな単純なプログラムでさえも なぜか プロセスが終了せずに残ってしまいます。

非デーモンスレッドが悪さをしているに違いありません。とりあえず すべてのスレッドを表示してみましょう。

MyApp.java
import javafx.application.Application; import javafx.stage.Stage; import java.util.Comparator; public class MyApp extends Application { public static void main(String[] args) { System.out.println("Hello, World!!"); //すべてのスレッドを表示する Thread.getAllStackTraces().keySet().stream() .sorted(Comparator.comparing(Thread::getId)) .forEach(t -> System.out.println( "Thread " + t.getId() + "¥t" + t.getName())); } @Override public void start(Stage stage) throws Exception { } }
コマンドプロンプト
C:¥temp>java MyApp Hello, World!! Thread 1 main Thread 2 Reference Handler Thread 3 Finalizer Thread 4 Signal Dispatcher Thread 5 Attach Listener Thread 11 Common-Cleaner Thread 12 QuantumRenderer-0 Thread 14 InvokeLaterDispatcher Thread 15 JavaFX Application Thread

なんてこった!

いつのまにか JavaFX Application Thread が生まれています。

どうやら javafx.application.Application の実装クラスをメインクラスとして起動すると JavaFX アプリケーション スレッドが勝手に起動してしまうようです。

…思い出しました。そういえば JavaFX アプリケーションでは main メソッドを省略できるという話を聞いたことがあります。Application 実装クラスをメインクラスとして指定すると main メソッドが定義されていないなくても JavaFX アプリケーションとして起動できるのだとか。

試してみましょう。main メソッドを持たない小さな JavaFX アプリケーションを作成してみます。

MyApp
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.stage.Stage; public class MyApp extends Application { @Override public void start(Stage stage) { Button button = new Button("Hello, World!!"); Scene scene = new Scene(button); stage.setScene(scene); stage.show(); } }

この MyApp クラスをメインクラスとして起動してみると…

たしかに main メソッドがなくても起動しました。

では main メソッドを追加するとどうなるのでしょうか? JavaFX アプリケーションの main メソッドでは launch メソッドを呼ぶのが通例ですが ここではあえて何もしません。

MyApp
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.stage.Stage; public class MyApp extends Application { public static void main(String[] args) { // do nothing } @Override public void start(Stage stage) { Button button = new Button("Hello, World!!"); Scene scene = new Scene(button); stage.setScene(scene); stage.show(); } }

これを実行すると JavaFX アプリケーション スレッドは生成されるが JavaFX アプリケーションは起動しないという中途半端な状態になりました。

main メソッドがなくても起動できる という仕組みと main メソッドが launch せずに終了する という振る舞いが競合しておかしくなっているみたいですね。

そもそも main メソッドなしで起動できる仕組みなんて JavaFX に必要だったんでしょうか? このわずか 3 行が省略できるだけですよ。

public static void main(String[] args) {
    launch(args);
}

なんだか main メソッドなしで起動できる JavaFX の仕組みに振り回されてしまったようです。何故 main メソッドのない Java プログラムを開始できるのか今も仕組みを理解できてはいません…

回避策 1

JavaFX アプリケーション スレッドが起動していると分かれば話は簡単です。Platform.exit() を呼び出して JavaFX アプリケーション スレッドを終了させればいいのです。

MyApp.java
import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; public class MyApp extends Application { public static void main(String[] args) { if(二重起動をチェック) { //同じアプリケーションが起動している場合は何もせずプロセスを終了する //なぜかJavaFXアプリケーションスレッドが起動してしまうので終了させる Platform.exit(); return; } launch(args); } @Override public void start(Stage stage) throws Exception { } }

これで プロセスが残留することはなくなりました。

しかし 必要のない JavaFX アプリケーション スレッドが勝手に起動して それを終了させなければならないというのもおかしな話です。

回避策 2

Application 実装クラスをメインクラスに指定すると JavaFX のおかしな仕組みが発動するので main メソッドを定義したメインクラスを別のクラスとして分離すれば この問題は回避できるはずです。

Main.java
import javafx.application.Application; public class Main { public static void main(String[] args) throws Exception { if(二重起動をチェック) { //同じアプリケーションが起動している場合は何もせずプロセスを終了する return; } Application.launch(MyApp.class, args); } }
MyApp.java
import javafx.application.Application; import javafx.stage.Stage; public class MyApp extends Application { @Override public void start(Stage stage) throws Exception { } }

上手くいきました。これで JavaFX アプリケーション スレッドが勝手に起動しないようになりました。

  • JavaFX main メソッドがなくても起動できる。
  • JavaFX main メソッドを書くなら必ず launch を呼ぶ。
  • JavaFX main メソッドで launch を呼ばないことがあるなら
    メインクラスは Application を継承しないようにする。

今日もまた少し JavaFX に詳しくなれたようです。

この記事を共有しませんか?