JDK-8237926 : Potential memory leak of model data in javafx.scene.control.ListView
  • Type: Bug
  • Component: javafx
  • Sub-Component: controls
  • Affected Version: openjfx11
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: x86_64
  • Submitted: 2020-01-27
  • Updated: 2020-03-06
  • Resolved: 2020-03-05
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other
openjfx15Fixed
Related Reports
Duplicate :  
Relates :  
Relates :  
Description
A DESCRIPTION OF THE PROBLEM :
The selection model of ListView keeps the most recently changed items of the underlying ObservableList as strong reference in "itemsListChange" of SelectedItemsReadOnlyObservableList.
The objects cannot be garbage collected even if they are removed  from the list.

Seems related to JDK-8227619 but the symptom is different - it is not the ListView that is expected to be freed in this scenario but the application's model objects.
(Test case adapted from JDK-8227619)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Create a ListView to an ObservableList of potentially big items, DON't select anything, empty the list.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The objects are not referenced any more and can be freed.
ACTUAL -
The objects are held by SelectedItemsReadOnlyObservableList.itemsListChange and will not be GC'ed.

---------- BEGIN SOURCE ----------
package listview;

import static org.junit.Assert.fail;

import java.lang.ref.WeakReference;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ListView;
import javafx.stage.Stage;

public class ListViewTest {

    static private class ContentObject {

    }

    static CountDownLatch startupLatch;

    private ListView<ContentObject> listView;

    private WeakReference<ContentObject> objectRef;

    public static class TestApp extends Application {

        @Override
        public void start(Stage stage) throws Exception {
            startupLatch.countDown();
        }
    }

    private static ObservableList<ContentObject> items = FXCollections.observableArrayList();

    public ListViewTest() {

    }

    @BeforeClass
    public static void initJavaFX() {
        startupLatch = new CountDownLatch(1);
        new Thread(() -> Application.launch(TestApp.class, (String[]) null)).start();
        try {
            if (!startupLatch.await(15, TimeUnit.SECONDS)) {
                fail("Timeout waiting for FX runtime to start");
            }
        } catch (InterruptedException ex) {
            fail("Unexpected exception: " + ex);
        }
    }

    @Before
    public void initListView() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        ContentObject contentObject = new ContentObject();
        items.add(contentObject);
        objectRef = new WeakReference<>(contentObject);

        Platform.runLater(() -> {
            listView = new ListView<>(items);
            latch.countDown();
        });

        if (!latch.await(15, TimeUnit.SECONDS)) {
            fail("Timeout waiting for FX listview initialization");
        }
    }

    @Test
    public void testGarbageCollect() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        Platform.runLater(() -> {
            items.clear();
            latch.countDown();
        });

        if (!latch.await(15, TimeUnit.SECONDS)) {
            fail("Timeout clear items");
        }

        for (int i = 0; i < 10; i++) {
            System.gc();
            System.runFinalization();

            if (objectRef.get() == null) {
                break;
            }

            try {
                System.out.println("Waiting");
                Thread.sleep(500);
            } catch (Exception ex) {
            }
        }
        Assert.assertTrue("Could not garbage collect content object", objectRef.get() == null);
        // Object is held by com.sun.javafx.scene.control.SelectedItemsReadOnlyObservableList.itemsListChange - added by
        // a list content change but when no selection index change occurs and it is not nulled in line #101
    }

    @Test
    public void testGarbageCollectOkWhenSelectionOccured() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);

        Platform.runLater(() -> {
            listView.getSelectionModel().selectFirst();
            latch.countDown();
        });
        if (!latch.await(15, TimeUnit.SECONDS)) {
            fail("Timeout select item");
        }

        Platform.runLater(() -> {
            items.clear();
            latch.countDown();
        });

        if (!latch.await(15, TimeUnit.SECONDS)) {
            fail("Timeout clear items");
        }

        for (int i = 0; i < 10; i++) {
            System.gc();
            System.runFinalization();

            if (objectRef.get() == null) {
                break;
            }

            try {
                System.out.println("Waiting");
                Thread.sleep(500);
            } catch (Exception ex) {
            }
        }
        Assert.assertTrue("Could not garbage collect content object", objectRef.get() == null);
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Have an item selected before clearing the list, this causes the selection index to change and the internally stored change to be cleaned up.


Comments
Changeset: 337ed722 Author: Ambarish Rapte <arapte@openjdk.org> Date: 2020-03-05 03:38:39 +0000 URL: https://git.openjdk.java.net/jfx/commit/337ed722
05-03-2020

Hello Kevin, I did test this issue with the proposed fix of JDK-8227619 and confirmed that it does not fix the issue.
28-01-2020

[~arapte] Does your proposed fix for JDK-8227619, which is currently under review, also fix this issue? If so, then this can be closed as a duplicate of that one. Otherwise, this will need to remain open as a separate (but related) bug.
28-01-2020