Hi there, I’ve recently been having some trouble with cleanup of FX resources in a JFXPanel inside a Swing application. I'm hoping you can point me in the right direction.
For some context, I’m working on a layered desktop application written entirely in Swing. The application has layers added and removed on demand by the user. As a consequence of this, layer dependencies should be cleaned up when a layer is removed to avoid memory leaks. Recently, a JFXPanel was added into one of these layers. However, I’m seeing that even when all references are cleared, the fx nodes and its dependencies can remain. This is with JavaFX 15. From debugging I’ve found that in the quantum toolkit, the ViewPainter.ROOT_PATHS still holds references. Oddly I’ve only ever been able to see the problem when a Text node is involved. The example at the bottom of this email shows the problem. To reproduce: launch the application, resize the panel, close the panel and take a heap dump. In the heap dump, you can see that the Dependency object held in the Node (NodeWithDependency) is eventually referenced by the static ViewPainter.ROOT_PATHS array. This reference seems to be linked through the properties of the Text node. This can take a few attempts to reproduce but I assume that's related to how the panel is resized. Note also that the scene in the JFXPanel is null’d out to ensure the Swing frame does not keep a reference to the fx scene. One workaround I found is to set prism.dirtyopts to false. This mitigates the problem by not using the paths. However, my impression of this property is that it should only ever be used for debugging and will cause performance degradation. So with all that said, I’m wondering if you could explain what I'm doing wrong here and/or if there is a more appropriate way to cleanup the JFXPanel in this scenario. Thanks in advance for any help. Brendan P.s. I attempted to post this last week without being subscribed to the mailing list. The message seems to be stuck awaiting moderator approval. My apologies if this shows up as a duplicate post. --- import java.util.concurrent.CountDownLatch; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; import javafx.scene.control.ScrollPane; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javax.swing.JFrame; import javax.swing.SwingUtilities; import static javax.swing.WindowConstants.DISPOSE_ON_CLOSE; public class JFXPanelCleanup { public static void main(String[] args) { SwingUtilities.invokeLater(JFXPanelCleanup::initAndShowGUI); startKeepAliveThread(); } private static void initAndShowGUI() { JFrame frame = new JFrame(); JFXPanel jfxPanel = new CleaningFxPanel(); frame.setContentPane(jfxPanel); frame.setSize(200, 100); frame.setVisible(true); frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE); Platform.runLater(() -> jfxPanel.setScene(new Scene(new NodeWithDependency(new Dependency())))); } private static void startKeepAliveThread() { new Thread(() -> { while (true) { try { //Force gc for good measure System.gc(); Thread.sleep(3_000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } private static class CleaningFxPanel extends JFXPanel { @Override public void removeNotify() { //Set the scene to null so that the invisible JFrame does not reference the scene invokeAndWaitOnPlatform(() -> setScene(null)); super.removeNotify(); } private void invokeAndWaitOnPlatform(Runnable runnable) { CountDownLatch latch = new CountDownLatch(1); Platform.runLater(() -> { runnable.run(); latch.countDown(); }); try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } public static class NodeWithDependency extends StackPane { private final Dependency dataDependecy; public NodeWithDependency(Dependency dependency) { this.dataDependecy = dependency; getChildren().add(new ScrollPane(new TextFlow(new Text("The quick brown fox jumps over the lazy dog")))); } } public static class Dependency { } }