SortedList hanging on to references & preventing GC

John Hendrikx john.hendrikx at gmail.com
Thu Dec 4 19:18:22 UTC 2025


This looks like a classic problem where unused elements in an array are
not nulled.  Looking at the code that updates the `size` field, there
are a few code paths that are not setting unused elements to null.  I
also saw no code that ever shrinks the arrays.  So it looks this
implementation is only half finished.

So, yes, I'd say this is a bug.

> I'm going to guess there's a very good reason for this behaviour.
Time pressure and insufficient testing.

--John

On 04/12/2025 19:40, Cormac Redmond wrote:
> Hi,
>
> I've traced a memory issue back to a SortedList (surprisingly), where
> it's hanging on to objects that could/should have been GC'd.
>
> SortedList's internal arrays will only grow (but not shrink) in line
> with the source ObservableList (I'm sure there are reasons for this).
> So even when your source ObservableList shrinks, SortedList is hanging
> on to references to objects the source list once contained, even
> though they're completely removed from the source list.
>
> This leads to a substantial waste of memory, especially when just one
> momentarily large dataset leads to a permanent spike in unnecessary
> memory usage for the remainder for the application's lifetime. I'm
> sure I'm not the first to raise this or ask about it & I'm going to
> guess there's a very good reason for this behaviour. But could someone
> explain this? I would have thought SortedList should/could remain as
> lean as the source list. FilteredList is somewhat similar, except it
> stores an array of ints (indexes), so less of a memory hit, but still
> pointless nonetheless.
>
> Example GIF + code below: setup a typical sortable table (so,
> ObservableList + FilteredList + SortableList), where the print button
> shows that when you add a lot of data, and remove it, SortedList
> retains references to all of the old objects. Some reflection is used
> to print that information.
>
> sorted_list_mem.gif
>
>
> Code to reproduce:
>
> public class SortedListMemWasteDemo extends Application {
>
>     record PotentialLargeData(String info) {}
>
>     public static void main(String[] args) {
>         launch(args);
>     }
>
>     @Override
>     public void start(Stage primaryStage) {
>         ObservableList<PotentialLargeData> masterData =
> FXCollections.observableArrayList(
>                 new PotentialLargeData("Initial info 1"),
>                 new PotentialLargeData("Initial info 2"));
>
>         TableColumn<PotentialLargeData, String> col = new
> TableColumn<>("Info");
>         col.setCellValueFactory(cellData -> new
> SimpleStringProperty(cellData.getValue().info()));
>        
>         TableView<PotentialLargeData> table = new TableView<>();
>         table.getColumns().add(col);
>
>         FilteredList<PotentialLargeData> filteredData = new
> FilteredList<>(masterData, item -> true);
>         SortedList<PotentialLargeData> sortedData = new
> SortedList<>(filteredData);
>         sortedData.comparatorProperty().bind(table.comparatorProperty());
>         table.setItems(sortedData);
>
>         Button loadManyBtn = new Button("Add 10,000 items");
>         loadManyBtn.setOnAction(e -> {
>             System.out.println("Adding 10000 items to master data");
>             masterData.clear();
>             for (int i = 0; i < 10000; i++) masterData.add(new
> PotentialLargeData("Info item " + i));
>         });
>
>         Button reduceBtn = new Button("Reduce to 2");
>         reduceBtn.setOnAction(e -> {
>             System.out.println("Reducing master data size to 2");
>             masterData.setAll(new PotentialLargeData("New info 1"),
> new PotentialLargeData("New info 2"));
>         });
>
>         Button printBtn = new Button("Print interal sizes");
>         printBtn.setOnAction(e -> {
>             try {
>                 System.out.println("Items the user sees: " +
> sortedData.size());
>                 int[] filtered = (int[]) getFieldValue(filteredData,
> "filtered");
>                 System.out.println("FilteredList.filtered length: " +
> filtered.length);
>                 // This is a hidden and significant waste of memory
>                 Object[] sorted = (Object[]) getFieldValue(sortedData,
> "sorted");
>                 System.out.println("SortedList.sorted length: " +
> sorted.length);
>             } catch (Exception ex) {
>                 throw new RuntimeException(ex);
>             }
>         });
>
>         primaryStage.setScene(new Scene(new VBox(table, loadManyBtn,
> reduceBtn, printBtn), 600, 400));
>         primaryStage.show();
>     }
>
>     private static Object getFieldValue(Object object, String
> fieldName) throws Exception {
>         Field field = object.getClass().getDeclaredField(fieldName);
>         field.setAccessible(true);
>         return field.get(object);
>     }
> }
>
>
> Kind Regards,
> Cormac
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20251204/d333facd/attachment-0001.htm>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: sorted_list_mem.gif
Type: image/gif
Size: 128014 bytes
Desc: not available
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20251204/d333facd/sorted_list_mem-0001.gif>


More information about the openjfx-dev mailing list