Утечки памяти при удалении объектов в JavaFX 3D

Я написал JavaFX-программу моделирования N-Body, которая отображает моделируемые тела в виде сфер. К сожалению, я борюсь с утечками памяти.

Я понял, что память, выделенная для сфер, не освобождается, когда сферы удаляются из их контейнера, даже если НЕТ ссылок (по крайней мере, из моего кода) на сферы.

Это поведение легко воспроизвести: следующий код представляет собой код JavaFX, автоматически сгенерированный Eclipse при создании проекта JavaFX. Я добавил кнопку и группу (которая служит контейнером для сфер). Нажатие на кнопку вызывает метод createSpheres, который добавляет с каждым щелчком 500 сфер в контейнер и отображает в консоли используемую (кучу) память.

    package application;

    import java.util.Random;
    import javafx.application.Application;
    import javafx.stage.Stage;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.layout.VBox;
    import javafx.scene.shape.Sphere;

    public class Main extends Application {
        @Override
        public void start(Stage primaryStage) {
            try {

                // --- User defined code -----------------
                VBox root = new VBox();
                Button btn = new Button("Add spheres...");
                Group container = new Group();
                root.getChildren().addAll(btn, container);
                btn.setOnAction(e -> {
                    createSpheres(container);
                });
                // ---------------------------------------
                Scene scene = new Scene(root,400,400);
                primaryStage.setScene(scene);
                primaryStage.show();
            } catch(Exception e) {
                e.printStackTrace();
            }
        }

        public static void main(String[] args) {
            launch(args);
        }

        // --- User defined code ------------------------------------------------------------------------
        // Each call increases the used memory although the container is cleared and the GC is triggered. 
        // The problem does not occur when all spheres have the same radius.
        // ----------------------------------------------------------------------------------------------
        private void createSpheres(Group container) {
                container.getChildren().clear();
                Runtime.getRuntime().gc();
                Random random = new Random();
                for (int i = 0; i < 500; i++) {
                    //double d = 100;                               // OK
                    double d = 100 * random.nextDouble() + 1;       // Problem 
                    container.getChildren().add(new Sphere(d));
                }
                System.out.printf("Spheres added. Total number of spheres: %d. Used memory: %d Bytes of %d Bytes.\n", 
                        container.getChildren().size(), 
                        Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),
                        Runtime.getRuntime().maxMemory());
        }
        // ----------------------------------------------------------------------------------------------
    }

Когда я запускаю программу, используемая память увеличивается с каждым щелчком, хотя контейнер очищается с помощью container.getChildren().clear() в начале метода (как и ожидалось, вызов удаляет сферы предыдущих нажатий кнопки, НО использованная память увеличивается еще больше). Следующий вызов Runtime.getRuntime().gc() также неэффективен (как и ожидалось, gc запускается, НО память не освобождается). Кажется, что на сферы все еще ссылаются откуда-то или если ресурсы не распоряжаются, предположительно, в коде JavaFX.

Вывод программы показан ниже: Я использовал максимальную кучу JVM 4 ГБ (-Xmx4g). Приблизительно после 30 щелчков достигается максимальный размер кучи, и приложение аварийно завершает работу с исключением нехватки памяти. Также показаны выходные данные Visual VM непосредственно перед добавлением сфер и после создания исключения. Последний показывает почти полный пул «старого поколения». На последнем рисунке показана куча во время добавления сфер. Хотя сборщик мусора выполнил 106 сборов, память не была освобождена.

Вывод консоли программы

Visual VM: куча перед добавлением сфер

Visual VM: куча после добавления сфер и возникновения исключения нехватки памяти

Visual VM: Heap во время добавления сфер

Примечательно, что поведение зависит от радиуса сфер. В примере кода каждая сфера имеет псевдослучайный радиус. Когда ОДИН и тот же радиус используется для ВСЕХ сфер (независимо от значения), например. new Sphere(100), используемая память остается неизменной, т.е. память освобождается должным образом! Но чем больше сфер с РАЗНЫМ радиусом, тем больше памяти потребляет приложение. На следующем рисунке показана память, когда сферы имеют одинаковые радиусы. Память освобождается.

Visual VM: Куча при добавлении сфер в случае одинаковых радиусов

Я использую JDK-9.0.1/JRE-9.0.1 и выпуск Eclipse Oxygen.1a (4.7.1a), идентификатор сборки: 20171005-1200, а также JRE1.8.0_151 и выпуск Eclipse Neon.3 (4.6.3), сборку идентификатор: 20170314-1500. Моя ОС — Windows 7 Максимальная, 64-разрядная.

Кто-нибудь знает, как я могу решить эту проблему (если это вообще возможно), или это действительно проблема с утечкой памяти JavaFX?


person user 9014097    schedule 27.11.2017    source источник
comment
Как предложено здесь, ваш профилировщик может выполнять сбор гаража; Я вижу, что результат тот же.   -  person trashgod    schedule 28.11.2017


Ответы (1)


Ответ на ваш вопрос можно найти в защищенном классе пакета с именем javafx.scene.shape.PredefinedMeshManager.

Каждый раз, когда вы создаете трехмерную форму Sphere/Box/Cylinder, объект TriangleMesh добавляется к HashMap на основе заданного ключа.

Для сфер этот ключ основан на их радиусе и количестве делений:

private static int generateKey(float r, int div) {
    int hash = 5;
    hash = 23 * hash + Float.floatToIntBits(r);
    hash = 23 * hash + div;
    return hash;
}

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

Это довольно удобно для обычного случая, когда у вас есть несколько сфер с одинаковым мешем: вам не нужно генерировать эти меши снова и снова, вы делаете это только один раз и кешируете меш.

Наоборот, если у вас разный радиус для сфер, ключ всегда будет разным, и при каждом клике вы будете добавлять в hashMap 500 новых объектов.

Пока вы очищаете контейнер со сферами, менеджер об этом не знает и не удаляет их из hashMap, поэтому счетчик увеличивается до тех пор, пока вы не получите исключение памяти.

С отражением мне удалось отслеживать размер sphereCache при добавлении 500 сфер до достижения исключения памяти:

Spheres added. Total number of spheres: 500. Used memory: 7794744 Bytes of 3817865216 Bytes.
sphereCache: javafx.scene.shape.PredefinedMeshManager@cb26fc7 Size: 500

Spheres added. Total number of spheres: 500. Used memory: 147193720 Bytes of 3817865216 Bytes.
sphereCache: javafx.scene.shape.PredefinedMeshManager@cb26fc7 Size: 1000

...

Spheres added. Total number of spheres: 500. Used memory: 3022528400 Bytes of 3817865216 Bytes.
sphereCache: javafx.scene.shape.PredefinedMeshManager@cb26fc7 Size: 11497

Spheres added. Total number of spheres: 500. Used memory: 3158410200 Bytes of 3817865216 Bytes.
sphereCache: javafx.scene.shape.PredefinedMeshManager@cb26fc7 Size: 11996

Spheres added. Total number of spheres: 500. Used memory: 3292418936 Bytes of 3817865216 Bytes.
sphereCache: javafx.scene.shape.PredefinedMeshManager@cb26fc7 Size: 12185

Exception in thread "JavaFX Application Thread"  java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3284)
at com.sun.javafx.collections.ObservableIntegerArrayImpl.ensureCapacity(ObservableIntegerArrayImpl.java:254)
at com.sun.javafx.collections.ObservableIntegerArrayImpl.setAllInternal(ObservableIntegerArrayImpl.java:131)
at com.sun.javafx.collections.ObservableIntegerArrayImpl.setAll(ObservableIntegerArrayImpl.java:156)
at javafx.scene.shape.Sphere.createMesh(Sphere.java:420)
at javafx.scene.shape.PredefinedMeshManager.getSphereMesh(PredefinedMeshManager.java:68)
at javafx.scene.shape.Sphere.impl_updatePeer(Sphere.java:157)
at javafx.scene.Node.impl_syncPeer(Node.java:503)
at javafx.scene.Scene$ScenePulseListener.synchronizeSceneNodes(Scene.java:2290)

Очевидно, что если бы у нас был доступ к этому кешу, мы могли бы предотвратить эту утечку памяти:

private void createSpheres(Group container) {
    container.getChildren().clear();
    if (sphereCache != null) {
        sphereCache.clear();
    }
    ...

}

Возможно, вы захотите сообщить о проблеме здесь.

person José Pereda    schedule 27.11.2017
comment
Большое спасибо за ваши усилия. Довольно хорошее объяснение проблемы. Я подал заявку, как вы предложили. - person user 9014097; 28.11.2017
comment
@jpereda Верен ли этот эффект утечки памяти кучи для пользовательских классов TriangleMesh или PredefinedMeshManager только пытается кэшировать объекты JavaFX3D Shape3D? - person Birdasaur; 22.01.2018
comment
@Birdasaur Кэш применяется только к встроенным фигурам (Box, Cylinder и Sphere). И кажется, что уже есть исправление для этой утечки памяти, ожидающее одобрения (см. issue< /а>). - person José Pereda; 22.01.2018