New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
8236840: Memory leak when switching ButtonSkin #147
Conversation
👋 Welcome back arapte! A progress list of the required criteria for merging this PR into |
Webrevs
|
/reviewers 2 |
@kevinrushforth |
In general, there are two approaches to avoiding listener-related memory leaks. One is to use a WeakListener; the other is to explicitly remove the listener when the object is removed or otherwise no longer needed. Using a WeakListener is certainly easier, but runs the risk of the listener being removed too early and not cleaning up after itself. I'm not suggesting that's the case here, but it is worth looking at. The one thing I would ask you to take a look at is whether it would matter if the old skin didn't call |
@aghaisas can you also review? |
The listener does not get early GCed here. I did verify this by creating large number of
This seems to be a bigger issue. |
Hi Kevin, Please take a look at the updated changes. |
modules/javafx.controls/src/main/java/javafx/scene/control/skin/ButtonSkin.java
Outdated
Show resolved
Hide resolved
@@ -171,6 +175,7 @@ public ButtonSkin(Button control) { | |||
|
|||
/** {@inheritDoc} */ | |||
@Override public void dispose() { | |||
getSkinnable().sceneProperty().removeListener(weakChangeListener); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1! Actually, looks like that manual remove is really needed - otherwise we get an NPE when removing the default button from its parent after the skin has been switched:
@Test
public void testDefaultButtonSwitchSkinAndRemove() {
Button button = new Button();
button.setDefaultButton(true);
Group root = new Group(button);
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
button.setSkin(new ButtonSkin1(button));
root.getChildren().remove(button);
}
Note: to see this NPE as failing test (vs. its printout on sysout), we need to re-wire the uncaughtExceptionHandler, see ComboBoxTest setup/cleanup for an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the test case, I did minor changes to it and included in the next commit.
The NPE can occur even without button.setDefaultButton(true);
.
Please take a look
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch! You are right, without explicit removal of the listener, the NPE happens always.
Actually, I had been sloppy and got distracted by the NPE from my actual goal which was to dig into Kevin's "not cleaning up after itself" and .. finally found a (concededly extreme corner-case :) where that's happening: when setting the skin to null. Two failing tests:
@Test
public void testDefaultButtonNullSkinReleased() {
Button button = new Button();
button.setDefaultButton(true);
Group root = new Group(button);
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
WeakReference<ButtonSkin> defSkinRef = new WeakReference<>((ButtonSkin)button.getSkin());
button.setSkin(null);
attemptGC(defSkinRef);
assertNull("skin must be gc'ed", defSkinRef.get());
}
@Test
public void testDefaultButtonNullSkinAcceleratorRemoved() {
Button button = new Button();
button.setDefaultButton(true);
Group root = new Group(button);
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
KeyCodeCombination key = new KeyCodeCombination(KeyCode.ENTER);
assertNotNull(scene.getAccelerators().get(key));
button.setSkin(null);
assertNull(scene.getAccelerators().get(key));
}
An explicitly cleanup in dispose makes them pass:
@Override
public void dispose() {
setDefaultButton(false);
setCancelButton(false);
getSkinnable().sceneProperty().removeListener(weakChangeListener);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have included this change and the test, with slight modification to include same test for Cancel button.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good :)
@arapte This change now passes all automated pre-integration checks. When the change also fulfills all project specific requirements, type
Since the source branch of this PR was last updated there have been 12 commits pushed to the ➡️ To integrate this PR with the above commit message, type |
/integrate |
@arapte The following commits have been pushed to master since your change was applied:
Your commit was automatically rebased without conflicts. Pushed as commit 418675a. |
ButtonSkin adds a
ChangeListener
toControl.sceneProperty()
which results in leaking theButtonSkin
itself when theButton
's skin is changed to a newButtonSkin
.Using a
WeakChangeListener
instead ofChangeListener
solves the issue.Please take a look.
Progress
Issue
Reviewers
Download
$ git fetch https://git.openjdk.java.net/jfx pull/147/head:pull/147
$ git checkout pull/147