[Rev 01] RFR: 8236840: Memory leak when switching ButtonSkin

Jeanette Winzenburg fastegal at openjdk.java.net
Mon Mar 30 10:38:23 UTC 2020


On Mon, 30 Mar 2020 07:21:38 GMT, Ambarish Rapte <arapte at openjdk.org> wrote:

>> modules/javafx.controls/src/main/java/javafx/scene/control/skin/ButtonSkin.java line 178:
>> 
>>> 177:     @Override public void dispose() {
>>> 178:         getSkinnable().sceneProperty().removeListener(weakChangeListener);
>>> 179:         super.dispose();
>> 
>> +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.
>
> 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

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);

-------------

PR: https://git.openjdk.java.net/jfx/pull/147


More information about the openjfx-dev mailing list