SortedList hanging on to references & preventing GC
Kevin Rushforth
kevin.rushforth at oracle.com
Thu Dec 4 19:52:55 UTC 2025
Since there is already a bug tracking this, can you add the additional
information and test case to the bug report?
-- Kevin
On 12/4/2025 11:44 AM, Andy Goryachev wrote:
> Spoke too soon: we already have
> https://bugs.openjdk.org/browse/JDK-8184166
> from 2017.
>
> Question: are there any more places where we don't null the unused
> entries?
>
> -andy
>
> *From: *openjfx-dev <openjfx-dev-retn at openjdk.org> on behalf of Andy
> Goryachev <andy.goryachev at oracle.com>
> *Date: *Thursday, December 4, 2025 at 11:40
> *To: *John Hendrikx <john.hendrikx at gmail.com>, openjfx-dev at openjdk.org
> <openjfx-dev at openjdk.org>
> *Subject: *Re: SortedList hanging on to references & preventing GC
>
> > Time pressure and insufficient testing.
>
> Alas! I can create a ticket, unless someone has already created one.
>
> -andy
>
>
> *From: *openjfx-dev <openjfx-dev-retn at openjdk.org> on behalf of John
> Hendrikx <john.hendrikx at gmail.com>
> *Date: *Thursday, December 4, 2025 at 11:18
> *To: *openjfx-dev at openjdk.org <openjfx-dev at openjdk.org>
> *Subject: *Re: SortedList hanging on to references & preventing GC
>
> 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/dd179ff5/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/dd179ff5/sorted_list_mem-0001.gif>
More information about the openjfx-dev
mailing list