-
Bug
-
Resolution: Fixed
-
P4
-
jfx11
-
x86_64
-
windows_7
ADDITIONAL SYSTEM INFORMATION :
* OpenJDK 11.0.2
* Redhat 6.8 and Windows 7 64bit. Confident this is agnostic of OS
A DESCRIPTION OF THE PROBLEM :
When the skin of a control is changed the old skin is not garbage collected.
I've traced this to strong reference in an event listener
Specifically com.sun.javafx.scene.control.skin.BehaviorSkinBase adds a listener for context menu request in its constructor. i.e. this line
control.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, contextMenuHandler);
However this listener is never removed in dispose()
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the test application
Monitor number of skin instances with JVisualVM or other profiler
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Only single instance of skin object, unused instances should be garbage collected
ACTUAL -
Constant increase in number of skin objects (i.e. they're not being garbage collected)
---------- BEGIN SOURCE ----------
import com.sun.javafx.scene.control.skin.ButtonSkin;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SkinMemoryLeak extends Application {
public static class ButtonSkinCustom1 extends ButtonSkin {
public ButtonSkinCustom1(Button button) {
super(button);
}
}
public static class ButtonSkinCustom2 extends ButtonSkin {
public ButtonSkinCustom2(Button button) {
super(button);
}
}
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
Button button = new Button("Foo");
Scene scene = new Scene(button);
primaryStage.setScene(scene);
primaryStage.show();
Timeline animation = new Timeline();
animation.getKeyFrames().add(new KeyFrame(Duration.seconds(1), event -> {
if (button.getSkin() instanceof ButtonSkinCustom2) {
button.setSkin(new ButtonSkinCustom1(button));
} else {
button.setSkin(new ButtonSkinCustom2(button));
}
}));
animation.setCycleCount(Animation.INDEFINITE);
animation.play();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Can be monkey patched by following in affected skin
@Override
public void dispose() {
try {
Field field = BehaviorSkinBase.class.getDeclaredField("contextMenuHandler");
field.setAccessible(true);
EventHandler handler = (EventHandler) field.get(this);
getSkinnable().removeEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, handler);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
super.dispose();
}
FREQUENCY : always
* OpenJDK 11.0.2
* Redhat 6.8 and Windows 7 64bit. Confident this is agnostic of OS
A DESCRIPTION OF THE PROBLEM :
When the skin of a control is changed the old skin is not garbage collected.
I've traced this to strong reference in an event listener
Specifically com.sun.javafx.scene.control.skin.BehaviorSkinBase adds a listener for context menu request in its constructor. i.e. this line
control.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, contextMenuHandler);
However this listener is never removed in dispose()
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the test application
Monitor number of skin instances with JVisualVM or other profiler
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Only single instance of skin object, unused instances should be garbage collected
ACTUAL -
Constant increase in number of skin objects (i.e. they're not being garbage collected)
---------- BEGIN SOURCE ----------
import com.sun.javafx.scene.control.skin.ButtonSkin;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SkinMemoryLeak extends Application {
public static class ButtonSkinCustom1 extends ButtonSkin {
public ButtonSkinCustom1(Button button) {
super(button);
}
}
public static class ButtonSkinCustom2 extends ButtonSkin {
public ButtonSkinCustom2(Button button) {
super(button);
}
}
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
Button button = new Button("Foo");
Scene scene = new Scene(button);
primaryStage.setScene(scene);
primaryStage.show();
Timeline animation = new Timeline();
animation.getKeyFrames().add(new KeyFrame(Duration.seconds(1), event -> {
if (button.getSkin() instanceof ButtonSkinCustom2) {
button.setSkin(new ButtonSkinCustom1(button));
} else {
button.setSkin(new ButtonSkinCustom2(button));
}
}));
animation.setCycleCount(Animation.INDEFINITE);
animation.play();
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Can be monkey patched by following in affected skin
@Override
public void dispose() {
try {
Field field = BehaviorSkinBase.class.getDeclaredField("contextMenuHandler");
field.setAccessible(true);
EventHandler handler = (EventHandler) field.get(this);
getSkinnable().removeEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, handler);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
super.dispose();
}
FREQUENCY : always
- blocks
-
JDK-8241364 ☂ Cleanup skin implementations to allow switching
- Open
- relates to
-
JDK-8209938 Default and Cancel button cause memory leak
- Resolved
-
JDK-8240506 TextFieldSkin/Behavior: misbehavior on switching skin
- Resolved