2

I encountered a memory leak I just can't fix in my code. Scenario: The user opens up a new window, in which a chart is drawn (in this case a LineChart). But when the window is closed, java.lang.ref.WeakReference and javafx.beans.property.BooleanPropertyBase$Listener remain in memory. Their number perfectly corresponds to the drawn data points (XYChart.Data). I just can't figure out how to get rid of them. In my code, many of these windows are opened and closed frequently, with 10k-100k data points per chart and memory fills up quite quickly.

I'm sure I made some stupid mistake, but I just can't find it. Help would be greatly appreciated!

Sample Code:

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Test extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(Test.class, args);
    }

    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250, Color.LIGHTGREEN);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Create stage");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {
                new CreateStage();
                primaryStage.toFront();

            }
        });
        root.getChildren().add(btn);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

class CreateStage {

    public CreateStage() {
        Stage stage = new Stage();
            stage.setTitle("Line Chart Sample");
        //Basic Chart attributes
        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("RT [minutes]");
        yAxis.setLabel("Intensity");
        LineChart<Number, Number> linechart = new LineChart(xAxis, yAxis);

                XYChart.Series newSeries = new XYChart.Series();

                List<XYChart.Data> list = new ArrayList<>();
                //just fill the chart with data points
                for (int j = 0; j < 10000; j++) {
                    float intensity = j;
                    float currentRT = j;

                    list.add(new XYChart.Data(currentRT, intensity));
                }
                newSeries.getData().addAll(list);

                // add new Series
                linechart.getData().add(newSeries);

        Scene scene  = new Scene(linechart,800,600);       


        stage.setScene(scene);
        stage.show();
    }
}  
S.T.K
  • 31
  • 6
  • Are you sure garbage collection is actually triggered. Furthermore I encountered a similar problem with `ImageView`/`Image`. Simply removing the `ImageView` from the scene graph did not make the `Image` available for garbage collection, but setting the `image` property of the `ImageView` did, so maybe setting the data of the chart to `null` helps... – fabian Jun 17 '16 at 17:51
  • Yes I'm sure, unfortunately. I think I already tried a similar thing, but I will look into it again, thanks! – S.T.K Jun 18 '16 at 17:49

2 Answers2

1

I was able to solve the problem. Calling

series.getData().clear();

on every XYChart.Series when closing the window get's rid of the memory leak. Things like javafx.scene.layout.CornerRadii, com.sun.javafx.sg.prism.NGRegion and javafx.scene.layout.Background still remain in memory, but get garbage collected eventually and don't build up.

Here's the fixed code:

import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Test extends Application {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(Test.class, args);
    }

    @Override
    public void start(final Stage primaryStage) {
        primaryStage.setTitle("Hello World");
        Group root = new Group();
        Scene scene = new Scene(root, 300, 250, Color.LIGHTGREEN);
        Button btn = new Button();
        btn.setLayoutX(100);
        btn.setLayoutY(80);
        btn.setText("Create stage");
        btn.setOnAction(new EventHandler<ActionEvent>() {

            public void handle(ActionEvent event) {
                CreateStage();
                primaryStage.toFront();

            }
        });
        root.getChildren().add(btn);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public void CreateStage() {

        Stage stage = new Stage();
        stage.setTitle("Line Chart Sample");
        //Basic Chart attributes
        NumberAxis xAxis = new NumberAxis();
        NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("RT [minutes]");
        yAxis.setLabel("Intensity");

        //linechart.getData().clear();
        LineChart<Number, Number> linechart = new LineChart(xAxis, yAxis);

        XYChart.Series newSeries = new XYChart.Series();

        List<XYChart.Data> list = new ArrayList<>();
        //just fill the chart with data points
        for (int j = 0; j < 10000; j++) {
            float intensity = j;
            float currentRT = j;

            list.add(new XYChart.Data(currentRT, intensity));
        }
        newSeries.getData().addAll(list);

        // add new Series
        linechart.getData().add(newSeries);

        Scene scene = new Scene(linechart, 800, 600);

        stage.setScene(scene);
        stage.show();

        //this fixes it
        stage.setOnCloseRequest(event -> {
            for (XYChart.Series series : linechart.getData()) {
                series.getData().clear();
            }

        });

    }
}

Thanks to @fabian for pointing me in the right direction.

S.T.K
  • 31
  • 6
0

This is most likely not a mistake from your side. I've encountered a similar problem, where some UI data was still left in the system memory after some parts of the UI were closed during runtime.

This is probably not the best solution to the problem, but you can try to reuse your windows.
You can do this by keeping track of all of your open windows and closed windows in separate ArrayLists.

If a user wants to close a window, you set the window to be invisible (instead of completely closing it) and move it into the ArrayList of closed windows. Once a new window is created, you can check your ArrayList of closed windows, if it contains one. If so, you can reuse this window (change the graph of this window and set the window visible) and move it from the ArrayList of closed windows to the ArrayList of open windows.

If you want to open a new window and there is no window inside the ArrayList of open windows, just create a new instance of your window class and add it to your ArrayList of open windows.
This is probably not the best work-around for your problem, but that is how I solved my problem, which is similar to yours.

Script47
  • 14,230
  • 4
  • 45
  • 66
Fabian B.
  • 321
  • 1
  • 2
  • 9
  • 1
    What is the difference between setting a stage to be invisible and closing it? (Obviously the main point of your suggestion still works; you could maintain a list/pool of closed windows, but I don't see the distinction between "invisible" and "closed".) – James_D Jun 17 '16 at 19:12
  • Good Idea! Thanks for your work-around Fabian B., you explained it very well! Now I can at least do something about this... – S.T.K Jun 18 '16 at 17:54
  • @James_D A "closed" window is not available to be displayed anymore. It was deleted by the application. An "invisible" window, on the other hand, is still in the system memory and ready to be displayed, but isn't displayed to the user at the moment. – Fabian B. Jun 19 '16 at 12:17
  • @FabianB. Unless I am missing something, there is no API in JavaFX that distinguishes these two states. There are `close()` and `hide()` methods, which are identical and allow the window to be shown again. – James_D Jun 19 '16 at 13:20
  • @James_D I thought, `close()` and `hide()` were different. At least that's what they were in Swing. But even if these two methods are identical, it should at least help a little bit, if the windows are reused instead of them being created and discarded for each and every shown window. – Fabian B. Jun 20 '16 at 14:31
  • The [docs](http://docs.oracle.com/javase/8/javafx/api/javafx/stage/Stage.html#close--) say otherwise. It *may* help; I strongly suspect that the memory leak is really in the Chart API (which shows every sign of having been written by an unsupervised intern who was having a bad day) rather than in the stage, however. – James_D Jun 20 '16 at 14:33
  • @James_D Have a look at this [video tutorial](https://youtu.be/DeK9DfXG5Tg) or at [this stackoverflow question](http://stackoverflow.com/questions/15520573/how-to-show-and-hide-a-window-in-javafx-2). If you use the `hide()` method, you can reuse a window, so my proposed work-around should work. If @S.T.K wants to try out the work-around on his application, we will see, if it helps to get around the memory leak. – Fabian B. Jun 20 '16 at 14:46
  • @Fabian B. The work-around didn't help, unfortunately, but thanks anyway! I was able to fix it and posted the code here. There was another problem in my application (not the sample code): calling "linechart.setCreateSymbols(false)" somehow creates references I couldn't get rid of, keeping the whole chart in memory forever. I solved it by using a css file instead (It seems like James_D 's theory about the origin of the Chart API is not that far off...) – S.T.K Jun 21 '16 at 06:41