JavaFx On children.clear 内存未释放

JavaFx On children.clear memory not released

我正在将 1000 个 ImageView 列表加载到 TilePane。然后,在一段时间后,我将删除 TilePane 的所有子项。但是在任务管理器中,我可以看到内存根本没有释放(消耗了 6GB RAM)。

这是代码

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();
    }

}

输出:


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

这仅用于演示。在我的实际项目中,我在SplitPane中有ListViewTilePane。每当我单击 ListView 中的项目(文件夹名称)时,我都必须通过清除以前的子项将图像(10 到 20 张图像)加载到 TilePane。但是没有清内存,一直在增加

我相信这里没有问题。

Windows 任务管理器不是测试 Java 内存消耗的可靠方法。 JVM 的工作方式是它会为堆留出一些内存。此内存分配由 Runtime.getRuntime().getTotalMemory() 报告。如果堆开始变满,它将为堆分配更多内存(最多 Runtime.getRuntime().maxMemory()),或者 运行 垃圾收集器删除未引用的对象。我相信在当前的实现中,JVM 不会将分配给堆的未使用内存释放回系统。您可以分别使用 JVM 选项 -Xms-Xmx 配置初始总内存和最大内存。

在您的应用程序中,当您加载图像时,它们当然会消耗内存。如果需要,堆大小将增长,直到最大内存分配,这是您在 Windows 任务管理器中看到的报告大小。

当您清除磁贴窗格时,在那之后的某个时刻(下一次渲染场景时),图像将从底层图形系统中物理移除,并且相关资源将被释放。此时,图像仍使用堆内存,但它们现在有资格进行垃圾回收。下一次垃圾收集器 运行s(如果需要更多内存,这将自动发生),该内存将被堆回收(而不是被系统回收);因此您会在 Runtime.getRuntime().freeMemory() 中看到更改,但不会在任务管理器中看到更改。

在您的测试代码中,您在清除平铺窗格的子列表之后调用 System.gc(),但在渲染脉冲发生之前,因此图像不符合垃圾收集条件;因此,您看不到 freeMemory() 的任何变化。 (sleep() 没有帮助,因为它阻塞了 FX Application 线程,阻止它渲染场景。)因为仍然有足够的空闲内存,即使加载了图像,GC 也不需要运行 并且不会自动调用。

这里有一个更完整的测试,它允许您实时监控堆内存使用情况,并通过按钮调用 GC。我在 Mac OS X(我的主机)和 4GB 堆 space(默认情况下)以及 Windows 10 运行ning 上测试了这个Oracle VirtualBox 中的 VM,具有 1GB 堆 space,图像数量减少到 400(由于内存分配给虚拟 OS)。两者都表现出相同的结果:加载图像时内存会增加。如果您加载新图像,GC 会自动启动并且内存消耗或多或少保持不变。如果你清除图像,内存没有变化,但如果你强制GC,堆内存被释放。

一旦堆作为达到最大内存分配。

所有这些行为都完全符合预期。

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/Whosebug/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();
    }

}