A DESCRIPTION OF THE PROBLEM :
FULL PRODUCT VERSION :
java version "8"
Java(TM) SE Runtime Environment (build 8+271)
Java HotSpot(TM) 64-Bit Server VM (build 8+271, mixed mode)
ADDITIONAL OS VERSION INFORMATION :
Reproduced on Windows 10, Ubuntu 20.04, Centos7.9
A DESCRIPTION OF THE PROBLEM :
When the content of a SwingNode is set to something else, the JLightweightFrame is never properly cleared.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Any code that dynamically replaces SwingNode existing content
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The JLightweightFrames will eventually be collected
ACTUAL -
The JLightweightFrames are not collected, causing massive memory leak in some uses case where the swing content of the SwingNode changes a lot.
REPRODUCIBILITY :
This bug can be reproduced in many ways.
---------- BEGIN SOURCE ----------
package com.example;
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.swing.*;
public class Main extends Application {
SwingNode m_swingNode;
Button m_btnChangeSwingNodeContent;
int m_changeCounter;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SwingNode JLightweightFrame Leak Demo");
m_swingNode = new SwingNode();
m_changeCounter = 0;
m_btnChangeSwingNodeContent = new Button("CHANGE SWINGNODE CONTENT");
m_btnChangeSwingNodeContent.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
m_changeCounter = m_changeCounter + 1;
JButton btn = new JButton("TEST BUTTON NB "+ m_changeCounter);
JPanel panel = new JPanel();
panel.add(btn);
m_swingNode.setContent(null); //Removing previous content if any
m_swingNode.setContent(panel);
}
});
StackPane root = new StackPane();
root.getChildren().add(m_swingNode);
root.getChildren().add(m_btnChangeSwingNodeContent);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
---------- END SOURCE ----------
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
You can reproduce it using this source code below:
package com.example;
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.swing.*;
public class Main extends Application {
SwingNode m_swingNode;
Button m_btnChangeSwingNodeContent;
int m_changeCounter;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SwingNode JLightweightFrame Leak Demo");
m_swingNode = new SwingNode();
m_changeCounter = 0;
m_btnChangeSwingNodeContent = new Button("CHANGE SWINGNODE CONTENT");
m_btnChangeSwingNodeContent.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
m_changeCounter = m_changeCounter + 1;
JButton btn = new JButton("TEST BUTTON NB "+ m_changeCounter);
JPanel panel = new JPanel();
panel.add(btn);
m_swingNode.setContent(null); //Removing previous content if any
m_swingNode.setContent(panel);
}
});
StackPane root = new StackPane();
root.getChildren().add(m_swingNode);
root.getChildren().add(m_btnChangeSwingNodeContent);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
When pressing the button "CHANGE SWINGNODE CONTENT" a new JButton shall appear with the following text "TEST BUTTON NB X" X being the number of times the button was clicked.
When tracking memory using JVisualVM, JLightweightFrame content should be equal to 1, the number of still displayed JLightweightFrame.
ACTUAL -
When pressing the button "CHANGE SWINGNODE CONTENT" a new JButton shall appear with the following text "TEST BUTTON NB X" X being the number of times the button was clicked.
When tracking memory using JVisualVM, JLightweightFrame content don't decrease when garbage collector is called.
---------- BEGIN SOURCE ----------
package com.example;
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javax.swing.*;
public class Main extends Application {
SwingNode m_swingNode;
Button m_btnChangeSwingNodeContent;
int m_changeCounter;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SwingNode JLightweightFrame Leak Demo");
m_swingNode = new SwingNode();
m_changeCounter = 0;
m_btnChangeSwingNodeContent = new Button("CHANGE SWINGNODE CONTENT");
m_btnChangeSwingNodeContent.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
m_changeCounter = m_changeCounter + 1;
JButton btn = new JButton("TEST BUTTON NB "+ m_changeCounter);
JPanel panel = new JPanel();
panel.add(btn);
m_swingNode.setContent(null); //Removing previous content if any
m_swingNode.setContent(panel);
}
});
StackPane root = new StackPane();
root.getChildren().add(m_swingNode);
root.getChildren().add(m_btnChangeSwingNodeContent);
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The problem is due to the JLightweightFrame is never released by de SwingNodeDisposer.
The SwingNodeDisposer holds an hard pointer to the JLightweightFrame that prevents its suppression.
I have modified the setContentImpl() function to use a WeakReference<JLightweightFrame> instead of an hard pointer and now memory is properly released.
...
WeakReference<JLightweightFrame> lwFramePtr = new WeakReference<JLightweightFrame>(lwFrame);
SwingNodeDisposer disposeRec = new SwingNodeDisposer(lwFramePtr);
Disposer.addRecord(this, disposeRec);
...
Contact me if you want the full SwingNode class or any additionnal details
FREQUENCY : always