JavaFx На детей. Ясная память не освобождена - PullRequest
0 голосов
/ 04 мая 2020

Я загружаю 1000 списков ImageViews в TilePane. А потом, по прошествии определенного времени, я удаляю всех детей из TilePane. Но в диспетчере задач я вижу, что память вообще не освобождается (используется 6 ГБ ОЗУ).

Это код

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

Это только для демонстрации. В моем реальном проекте у меня есть ListView и TilePane в SplitPane. Всякий раз, когда я щелкаю элемент (имя папки) в ListView, мне приходится загружать изображения (от 10 до 20 изображений) в TilePane путем очистки предыдущих дочерних элементов. Но это не очищает память и продолжает увеличиваться.

1 Ответ

1 голос
/ 04 мая 2020

Я вполне уверен, что здесь нет проблем.

Диспетчер задач Windows не является надежным способом проверки Java потребления памяти. JVM работает так, что он выделяет некоторую память для кучи. Об этом распределении памяти сообщает Runtime.getRuntime().getTotalMemory(). Если куча начинает заполняться, она либо выделяет кучу памяти (до Runtime.getRuntime().maxMemory()), либо запускает сборщик мусора для удаления объектов, на которые нет ссылок. Я полагаю, что в текущих реализациях JVM не освободит неиспользуемую память, назначенную куче, обратно в систему. Вы можете настроить начальный общий и максимальный объем памяти, используя параметры JVM -Xms и -Xmx соответственно.

В вашем приложении при загрузке изображений они, конечно, будут занимать память. При необходимости размер кучи будет увеличиваться, вплоть до максимального выделения памяти, и это тот размер, который вы видите, сообщенный диспетчером задач Windows.

Когда вы очищаете свою панель плиток, в некоторых случаях Вскоре после этого (когда сцена будет визуализирована в следующий раз), изображения будут физически удалены из базовой графической системы, и связанные ресурсы будут освобождены. На этом этапе куча памяти все еще используется изображениями, но теперь они пригодны для сбора мусора. В следующий раз, когда будет запущен сборщик мусора (что произойдет автоматически, если потребуется больше памяти), эта память будет восстановлена ​​кучей (но не системой); поэтому вы увидите изменения в Runtime.getRuntime().freeMemory(), но не в диспетчере задач.

В своем тестовом коде вы вызываете System.gc() после очистки списка дочерних элементов панели листов, но до того, как произойдет импульс рендеринга, поэтому изображения не подходят для сбора мусора; следовательно, вы не видите изменений в freeMemory(). (sleep() не помогает, потому что он блокирует поток приложения FX, не позволяя ему визуализировать сцену.) Поскольку по-прежнему много свободной памяти, даже с загруженными изображениями, G C не требуется для запуска и не вызывается автоматически.

Вот более полный тест, который позволяет отслеживать использование памяти кучи в режиме реального времени и вызывать G C с помощью кнопки. Я проверил это на Ma c OS X (моей хост-машине) с 4 ГБ пространства кучи (по умолчанию), а также на Windows 10, работающем на виртуальной машине в Oracle VirtualBox, с объемом кучи 1 ГБ и количеством образы уменьшены до 400 (из-за выделения памяти для виртуальной ОС). Оба показали одинаковые результаты: память увеличивается при загрузке изображений. Если вы загружаете fre sh изображений, G C включается автоматически, и потребление памяти остается более или менее постоянным. Если вы очистите образы, в памяти ничего не изменится, но если затем принудительно вызвать G C, память кучи будет освобождена.

Инструменты ОС (Activity Monitor на Ma c и диспетчер задач на 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();
    }

}
...