-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
P3
-
Affects Version/s: jfx21, jfx25, jfx26
-
Component/s: javafx
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
openjdk version "17.0.17" 2025-10-21
OpenJDK Runtime Environment (build 17.0.17+10-Debian-1deb12u1)
OpenJDK 64-Bit Server VM (build 17.0.17+10-Debian-1deb12u1, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
XYChart accumulates com.sun.javafx.binding.FlatMappedBinding and referenced objects for removed data points.
To reproduce: simply use a chart type like ScatterChart which automatically creates a node (StackPane) for each data point and continuously add and remove data points until you run out of memory.
Also affected should be other chart types that define a node or when setting a custom node to the data.
Analysis: Bindings for accessibility text in https://github.com/openjdk/jfx/blob/935c7b797d79407d741735324313684617d1292d/modules/javafx.controls/src/main/java/javafx/scene/chart/XYChart.java#L1370 to properties of the Series (and sub objects) are bound but never freed.
Workaround: node.accessibleTextProperty().unbind() or setting the node to null is not sufficient as a work around. I only found it helpful to replace the whole Series object.
Tested JDK 17, JDK 21
20.0.2 ok
21.0.7 fail
21.0.9 fail
23.0.2 fail
Supposedly relevant commit
8298382: JavaFX ChartArea Accessibility Reader
https://github.com/openjdk/jfx/commit/33f1f629c5df9f8e03e81e360730536cde0a8f53
REGRESSION : Last worked in version 20
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the example with -Xmx200m -XX:+HeapDumpOnOutOfMemoryError (or other reasonable values)
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Shows a chart of a hand full of data points. Runs forever (or an hour for the sake of testing).
ACTUAL -
Crashes with java.lang.OutOfMemoryError: Java heap space in under ten minutes.
---------- BEGIN SOURCE ----------
import java.util.concurrent.Semaphore;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* <pre>
* Run with: java -Xmx200m -XX:+HeapDumpOnOutOfMemoryError
*
* Accumulates com.sun.javafx.binding.FlatMappedBinding objects.
*
* Tested JDK 17, JDK 21
*
* 20.0.2 ok
* 21.0.7 fail
* 21.0.9 fail
* 23.0.2 fail
*
* Supposedly relevant commit
* 8298382: JavaFX ChartArea Accessibility Reader
* https://github.com/openjdk/jfx/commit/33f1f629c5df9f8e03e81e360730536cde0a8f53
* </pre>
*
* @author Benjamin Peter
*/
public class XYChartOom extends Application {
// Set to true to implement a (bad) workaround
private static final boolean WITH_WORKAROUND = false;
// Exact values below are NOT relevant for the leak
private static final int SLEEP_BEFORE_START_MS = 20_000;
private static final int BATCH_SIZE = 20;
private static final int Y_UPPER_BOUND = 10;
public static void main(final String[] args) {
launch(args);
}
@Override
public void start(final Stage primaryStage) throws InterruptedException {
System.out.println("Initializing");
primaryStage.setTitle("XY Chart OOM - bpeter");
// RELEVANT: Using a chart type that creates a node per data item, like ScatterChart!
final ScatterChart<Number, Number> chart = new ScatterChart<>( //
new NumberAxis(), //
new NumberAxis(0, Y_UPPER_BOUND, 1));
chart.setAnimated(false);
chart.getData().add(createNewSeries());
primaryStage.setScene(new Scene(new VBox(chart), 800, 400));
primaryStage.show();
System.out.println("Initialized UI");
final Thread modificationsThread = new Thread(() -> {
final Semaphore semaphore = new Semaphore(1); // Just make sure to not overwhelm FX thread
System.out.println(
"Starting to modify data set in " + SLEEP_BEFORE_START_MS + " ms. Please prepare vm monitoring.");
try {
Thread.sleep(SLEEP_BEFORE_START_MS);
} catch (final InterruptedException e) {
System.err.println("Interrupted");
return;
}
System.out.println("Starting");
for (int i = 0; i < Integer.MAX_VALUE; i++) {
final int ii = i; // Must be final for use in lambda
System.out.println(ii + " Waiting to modify");
try {
semaphore.acquire();
} catch (final InterruptedException e) {
System.err.println(ii + "Interrupted");
return;
}
System.out.println(ii + " Going to modifying");
Platform.runLater(() -> {
final var data = chart.getData().get(0).getData();
/* RELEVANT: Removed data items do not cause listeners from XYChart (ScatterChart) to be removed. See
*
* https://github.com/openjdk/jfx/blob/7e2c0d435a46cd4df9fa8d215f2a943d21cab7d9/modules/javafx.controls/src/main/java/javafx/scene/chart/XYChart.java#L1370
*/
data.clear();
IntStream.range(0, BATCH_SIZE) //
.mapToObj(di -> new Data<Number, Number>(di, di / (double) BATCH_SIZE * Y_UPPER_BOUND)) //
.forEach(d -> data.add(d));
System.out.println(ii + " Modification done, data set size: " + data.size());
/* WORKAROUND: replace series to free listeners that are still bound despite all data being removed */
if (WITH_WORKAROUND) {
final var series = chart.getData().get(0);
final var seriesNew = createNewSeries();
seriesNew.setData(series.getData());
chart.getData().set(0, seriesNew);
}
semaphore.release();
});
}
}, "modify_dataset_thread");
modificationsThread.setDaemon(true);
modificationsThread.start();
}
private final static XYChart.Series<Number, Number> createNewSeries() {
final XYChart.Series<Number, Number> chainStateSeries = new XYChart.Series<>();
chainStateSeries.setName("Data series");
return chainStateSeries;
}
}
---------- END SOURCE ----------
openjdk version "17.0.17" 2025-10-21
OpenJDK Runtime Environment (build 17.0.17+10-Debian-1deb12u1)
OpenJDK 64-Bit Server VM (build 17.0.17+10-Debian-1deb12u1, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
XYChart accumulates com.sun.javafx.binding.FlatMappedBinding and referenced objects for removed data points.
To reproduce: simply use a chart type like ScatterChart which automatically creates a node (StackPane) for each data point and continuously add and remove data points until you run out of memory.
Also affected should be other chart types that define a node or when setting a custom node to the data.
Analysis: Bindings for accessibility text in https://github.com/openjdk/jfx/blob/935c7b797d79407d741735324313684617d1292d/modules/javafx.controls/src/main/java/javafx/scene/chart/XYChart.java#L1370 to properties of the Series (and sub objects) are bound but never freed.
Workaround: node.accessibleTextProperty().unbind() or setting the node to null is not sufficient as a work around. I only found it helpful to replace the whole Series object.
Tested JDK 17, JDK 21
20.0.2 ok
21.0.7 fail
21.0.9 fail
23.0.2 fail
Supposedly relevant commit
8298382: JavaFX ChartArea Accessibility Reader
https://github.com/openjdk/jfx/commit/33f1f629c5df9f8e03e81e360730536cde0a8f53
REGRESSION : Last worked in version 20
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the example with -Xmx200m -XX:+HeapDumpOnOutOfMemoryError (or other reasonable values)
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Shows a chart of a hand full of data points. Runs forever (or an hour for the sake of testing).
ACTUAL -
Crashes with java.lang.OutOfMemoryError: Java heap space in under ten minutes.
---------- BEGIN SOURCE ----------
import java.util.concurrent.Semaphore;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* <pre>
* Run with: java -Xmx200m -XX:+HeapDumpOnOutOfMemoryError
*
* Accumulates com.sun.javafx.binding.FlatMappedBinding objects.
*
* Tested JDK 17, JDK 21
*
* 20.0.2 ok
* 21.0.7 fail
* 21.0.9 fail
* 23.0.2 fail
*
* Supposedly relevant commit
* 8298382: JavaFX ChartArea Accessibility Reader
* https://github.com/openjdk/jfx/commit/33f1f629c5df9f8e03e81e360730536cde0a8f53
* </pre>
*
* @author Benjamin Peter
*/
public class XYChartOom extends Application {
// Set to true to implement a (bad) workaround
private static final boolean WITH_WORKAROUND = false;
// Exact values below are NOT relevant for the leak
private static final int SLEEP_BEFORE_START_MS = 20_000;
private static final int BATCH_SIZE = 20;
private static final int Y_UPPER_BOUND = 10;
public static void main(final String[] args) {
launch(args);
}
@Override
public void start(final Stage primaryStage) throws InterruptedException {
System.out.println("Initializing");
primaryStage.setTitle("XY Chart OOM - bpeter");
// RELEVANT: Using a chart type that creates a node per data item, like ScatterChart!
final ScatterChart<Number, Number> chart = new ScatterChart<>( //
new NumberAxis(), //
new NumberAxis(0, Y_UPPER_BOUND, 1));
chart.setAnimated(false);
chart.getData().add(createNewSeries());
primaryStage.setScene(new Scene(new VBox(chart), 800, 400));
primaryStage.show();
System.out.println("Initialized UI");
final Thread modificationsThread = new Thread(() -> {
final Semaphore semaphore = new Semaphore(1); // Just make sure to not overwhelm FX thread
System.out.println(
"Starting to modify data set in " + SLEEP_BEFORE_START_MS + " ms. Please prepare vm monitoring.");
try {
Thread.sleep(SLEEP_BEFORE_START_MS);
} catch (final InterruptedException e) {
System.err.println("Interrupted");
return;
}
System.out.println("Starting");
for (int i = 0; i < Integer.MAX_VALUE; i++) {
final int ii = i; // Must be final for use in lambda
System.out.println(ii + " Waiting to modify");
try {
semaphore.acquire();
} catch (final InterruptedException e) {
System.err.println(ii + "Interrupted");
return;
}
System.out.println(ii + " Going to modifying");
Platform.runLater(() -> {
final var data = chart.getData().get(0).getData();
/* RELEVANT: Removed data items do not cause listeners from XYChart (ScatterChart) to be removed. See
*
* https://github.com/openjdk/jfx/blob/7e2c0d435a46cd4df9fa8d215f2a943d21cab7d9/modules/javafx.controls/src/main/java/javafx/scene/chart/XYChart.java#L1370
*/
data.clear();
IntStream.range(0, BATCH_SIZE) //
.mapToObj(di -> new Data<Number, Number>(di, di / (double) BATCH_SIZE * Y_UPPER_BOUND)) //
.forEach(d -> data.add(d));
System.out.println(ii + " Modification done, data set size: " + data.size());
/* WORKAROUND: replace series to free listeners that are still bound despite all data being removed */
if (WITH_WORKAROUND) {
final var series = chart.getData().get(0);
final var seriesNew = createNewSeries();
seriesNew.setData(series.getData());
chart.getData().set(0, seriesNew);
}
semaphore.release();
});
}
}, "modify_dataset_thread");
modificationsThread.setDaemon(true);
modificationsThread.start();
}
private final static XYChart.Series<Number, Number> createNewSeries() {
final XYChart.Series<Number, Number> chainStateSeries = new XYChart.Series<>();
chainStateSeries.setName("Data series");
return chainStateSeries;
}
}
---------- END SOURCE ----------