SortedList hanging on to references & preventing GC

John Hendrikx john.hendrikx at gmail.com
Fri Dec 5 04:20:37 UTC 2025


On 04/12/2025 21:21, Cormac Redmond wrote:
> Hi John,
>
> Thanks for the prompt response. Yes, I looked into the code
> afterwards, and I can see that it is just fairly oblivious to the
> waste. I was looking to override with nulls myself, but there were too
> many privates.
>
> The only solution (without re-writing my own impl) is to re-create the
> SortedList every time the dataset is smaller, or to create some sort
> of DTO that does not link to the original real objects in any way (or
> maybe use WeakReferences). In my case, I could have 10,000 rows where
> each item's underlying memory is around 500KB on the heap. That's
> instantly 5GB I cannot re-claim, or example.
>
> That code with the "bug" /mostly/ hasn't changed in 10+ years. Can it
> be fixed? It's such a fundamental class, adding workarounds
> (everywhere, forever) would seem a shame.

It definitely can be fixed.  Nothing should be relying on the current
behavior, as it is not specified to hold on to stale references.

You could try submit a PR with a fix, or wait until this becomes a
priority.  I myself may also do a fix when I have time, as it is pretty
low hanging fruit and self contained.

You can "test" a fixed implementation by just making a copy of the
class. If you're not in a modular environment, you can even replace the
class (in the same package it is in) with a fixed version until FX fixes
it.  This is how I usually test and fix (deep) internal problems without
having to build FX locally at all.

--John


>
>
>
>
> Kind Regards,
> Cormac
>
> On Thu, 4 Dec 2025 at 20:01, John Hendrikx <john.hendrikx at gmail.com>
> wrote:
>
>     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/20251205/666cbc4e/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/20251205/666cbc4e/sorted_list_mem-0001.gif>


More information about the openjfx-dev mailing list