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.