【问题标题】:JavaFx On children.clear memory not releasedJavaFx On children.clear 内存未释放
【发布时间】:2020-05-04 14:02:16
【问题描述】:

我正在将 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。但它并没有清除内存并不断增加。

【问题讨论】:

  • hmm .. 看起来像一个错误
  • 1.任务管理器只是报告分配给 Java 堆的内存量;这并不意味着堆空间都被使用了。 2. 除非确实需要,否则垃圾收集器不会释放堆中的空间。那么,Runtime.getRuntime().freeMemory()Runtime.getRuntime().totalMemory() 会报告什么,如果调用 System.gc () 会发生什么?
  • @James_D 我在问题中添加了输出
  • 该测试不正确(尽管问题可能仍然存在):您对System.gc() 的调用可能发生在您调用tilePane.getChildren().clear() 之前。清除子列表后,将System.out.println()System.gc() 调用移动到Platform.runLater(),看看是否会改变输出。
  • 但是您对System.gc() 的调用仍会(可能)在您调用清除子列表之前发生。 Platform.runLater(...) 安排代码在 FX 应用程序线程上运行,因此在执行之前可能会有一个小暂停。之后的代码立即发生在该线程中。所以当你调用System.gc()时,图片仍然被UI引用,GC也无能为力。清除图像后,您只需闲逛一会儿,由于仍有大量可用内存,因此不会调用 GC,因此永远不会清理内存。

标签: javafx memory-leaks javafx-8


【解决方案1】:

我相当确信这里没有问题。

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

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

当您清除平铺窗格时,此后不久(下一次渲染场景时),图像将从底层图形系统中物理移除,相关资源将被释放.此时,图像仍在使用堆内存,但它们现在可以进行垃圾回收了。下次垃圾收集器运行时(如果需要更多内存,这将自动发生),该内存将被堆回收(但不被系统回收);所以你会在 Runtime.getRuntime().freeMemory() 中看到变化,但不会在任务管理器中看到。

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

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

一旦堆达到最大内存分配,操作系统工具(Mac 上的活动监视器和 Windows 上的任务管理器)报告或多或少的恒定内存使用情况。

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

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

}

【讨论】:

  • 我不确定我的示例代码,为什么它消耗 6GB 内存并在清除孩子后继续显示相同的数字(15 分钟)。但是在我的实际代码中,我可以通过删除imageView.setCache(true) 来解决这个问题。感谢您提供信息和代码。它有助于理解很多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-09-05
  • 1970-01-01
  • 2019-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多