Search code examples
javafxmemory-leaksjavafx-8

JavaFx On children.clear memory not released


I am loading 1000 list of ImageViews to TilePane. And then, after certain period, I am removing all the children of the TilePane. But In the Task Manager, I can see the memory not release at all(consumed 6GB RAM).

This is the code

package application;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.TilePane;

public class Controller implements Initializable {

    @FXML
    private TilePane tilePane;

    @Override
    public void initialize(URL arg0, ResourceBundle arg1) {
        File rootPathFile = new File("C:\\Users\\Flower\\3D Objects\\New folder");
        File[] fileList = rootPathFile.listFiles();

        ObservableList<ImageView> btnList = FXCollections.observableArrayList();

        for (File file : fileList) {
            Image img = new Image(file.toURI().toString());
            ImageView imgView = new ImageView(img);
            imgView.setFitWidth(200);
            imgView.setFitHeight(300);
            btnList.add(imgView);
        }
        tilePane.getChildren().addAll(btnList);

        new Thread(() -> {
            try {
                Thread.sleep(10000);
                System.out.println("\nAfter adding 1000 images:");
                System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory());
                System.out.println("Totel Memory: " + Runtime.getRuntime().totalMemory());

                Thread.sleep(15000);
                Platform.runLater(() -> {
                    try {
                        tilePane.getChildren().clear();

                        Thread.sleep(5000); // to ensure memory changes
                        System.out.println("\nAfter clearing children:");
                        System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory());
                        System.out.println("Totel Memory: " + Runtime.getRuntime().totalMemory());

                        System.gc();
                        Thread.sleep(10000);

                        System.out.println("\nAfter GC() and 10 seconds sleep");
                        System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory());
                        System.out.println("Totel Memory: " + Runtime.getRuntime().totalMemory());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

Output:


After adding 1000 images:
Free Memory: 1044423528
Totel Memory: 4572839936

After clearing children:
Free Memory: 1035263112
Totel Memory: 4572839936

After GC() and 10 seconds sleep
Free Memory: 1045599688
Totel Memory: 4572839936

This is only for demo. In my actual project, I have ListView and TilePane in the SplitPane. Whenever I click item(folder name) in ListView, I have to load images(10 to 20 images) to the TilePane by clearing previous children. But its not clearing the memory and keeps increasing.


Solution

  • I'm fairly convinced there is no issue here.

    The Windows Task Manager is not a reliable way to test Java memory consumption. The way the JVM works is that it will set aside some memory for the heap. This memory allocation is reported by Runtime.getRuntime().getTotalMemory(). If the heap starts to get full, it will either allocate more memory to the heap (up to Runtime.getRuntime().maxMemory()), or run the garbage collector to remove unreferenced objects. I believe in current implementations the JVM will not release unused memory assigned to the heap back to the system. You can configure the initial total and max memory using the JVM options -Xms and -Xmx, respectively.

    In your application, as you load images, they will of course consume memory. If needed, the heap size will grow, up to the max memory allocation, and this is the size you see reported by the Windows Task Manager.

    When you clear your tile pane, at some point soon after that (when the scene is next rendered), the images will be physically removed from the underlying graphics system, and associated resources will be released. At this point, the heap memory is still used by the images, but they are now eligible for garbage collection. The next time the garbage collector runs (which will happen automatically if more memory is needed), that memory will be reclaimed by the heap (but not by the system); so you will see changes in Runtime.getRuntime().freeMemory(), but not in the Task Manager.

    In your test code, you call System.gc() after clearing the child list of the tile pane, but before the rendering pulse happens, so the images are not eligible for garbage collection; consequently you see no changes to the freeMemory(). (The sleep() doesn't help, because it blocks the FX Application thread, preventing it from rendering the scene.) Because there is still plenty of free memory, even with the images loaded, the GC doesn't need to run and isn't invoked automatically.

    Here's a more complete test, that allows you to monitor the heap memory usage in real time, and invoke the GC from a button. I tested this on Mac OS X (my host machine) with 4GB of heap space (by default), and also on Windows 10 running on a VM in Oracle VirtualBox, with 1GB heap space and the number of images reduced to 400 (due to memory allocation to the virtual OS). Both exhibited the same results: the memory grows when you load images. If you load fresh images, the GC kicks in automatically and the memory consumption stays more or less constant. If you clear the images, there's no change in the memory, but if you then force the GC, the heap memory is released.

    The OS tools (Activity Monitor on Mac and Task Manager on Windows) report a more-or-less constant memory usage, once the heap as hit the max memory allocation.

    All this behavior is exactly as expected.

    import java.util.ArrayList;
    import java.util.List;
    
    import javafx.animation.AnimationTimer;
    import javafx.application.Application;
    import javafx.concurrent.Task;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.ProgressIndicator;
    import javafx.scene.control.ScrollPane;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    import javafx.scene.image.PixelReader;
    import javafx.scene.image.WritableImage;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.scene.layout.TilePane;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    
    /**
     * JavaFX App
     */
    public class App extends Application {
    
        private static final String IMAGE_URL = "https://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png";
    
        private Image image ;
    
        @Override
        public void init() {
            // download image:
            image = new Image(IMAGE_URL, false);
        };
    
    
        @Override
        public void start(Stage primaryStage)  {
            TilePane imagePane = new TilePane();
    
            Button load = new Button("Load Images");
    
    
            Button clear = new Button("Clear");
            clear.setOnAction(e -> imagePane.getChildren().clear());
    
            Button gc = new Button("GC");
            gc.setOnAction(e -> System.gc());
    
            HBox buttons = new HBox(load, clear, gc);
            buttons.setSpacing(5);
    
            load.setOnAction(e -> {
                imagePane.getChildren().clear();
                ProgressIndicator progressIndicator = new ProgressIndicator(ProgressIndicator.INDETERMINATE_PROGRESS);
                TilePane.setAlignment(progressIndicator, Pos.CENTER);
                imagePane.getChildren().add(progressIndicator);
                buttons.setDisable(true);
    
                PixelReader reader = image.getPixelReader();
                int w = (int)image.getWidth();
                int h = (int)image.getHeight();
    
                Task<List<Image>> createImagesTask = new Task<>() {
                    @Override
                    public List<Image> call() {
    
                        // Create 1000 new copies of the image we downloaded
                        // Note: Never do this in real code, you can reuse
                        // the same image for multiple views. This is just
                        // to mimic loading 1000 different images into memory.
                        List<Image> images = new ArrayList<>(1000);
                        for (int i = 0 ; i < 1000 ; i++) {
                            images.add( new WritableImage(reader, w, h));
                            updateProgress(i, 1000);
                        }
                        return images ;
                    }
    
                };
    
                progressIndicator.progressProperty().bind(createImagesTask.progressProperty());
    
                createImagesTask.setOnSucceeded(evt -> {
    
                    // Create images views from each of the copies of the image,
                    // and add them to the image pane:
                    imagePane.getChildren().clear();
                    for (Image img : createImagesTask.getValue()) {
                        ImageView imageView = new ImageView(img);
                        imageView.setFitWidth(200);
                        imageView.setPreserveRatio(true);
                        imagePane.getChildren().add(imageView);
                    }
                    buttons.setDisable(false);
                });
    
                Thread t = new Thread(createImagesTask);
                t.setDaemon(true);
                t.start();
            });
    
    
            // Labels to display memory usage:            
            Label freeMem = new Label("Free memory:");
            Label usedMem = new Label("Used memory:");
            Label totalMem = new Label("Total memory:");
            Label maxMem = new Label("Max memory:");
            VBox memoryMon = new VBox(freeMem, usedMem, totalMem, maxMem);
            memoryMon.setSpacing(5);
    
    
            // use an AnimationTimer to update the memory usage on each
            // rendering pulse:
            AnimationTimer memTimer = new AnimationTimer() {
    
                @Override
                public void handle(long now) {
                    long free = Runtime.getRuntime().freeMemory();
                    long total = Runtime.getRuntime().totalMemory();
                    long max = Runtime.getRuntime().maxMemory();
                    freeMem.setText(String.format("Free memory: %,d", free));
                    usedMem.setText(String.format("Used memory: %,d", total-free));
                    totalMem.setText(String.format("Total memory: %,d", total));
                    maxMem.setText(String.format("Max memory: %,d", max));
                }
    
            };
            memTimer.start();
    
            BorderPane root = new BorderPane();
            root.setCenter(new ScrollPane(imagePane));
            root.setTop(buttons);
            root.setBottom(memoryMon);
    
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.setWidth(800);
            primaryStage.setHeight(500);
            primaryStage.show();
        }
    
    
    
        public static void main(String[] args) {
            launch();
        }
    
    }