Memory leaks when removing objects in JavaFX 3D

1k Views Asked by At

I have written an N-Body-simulation JavaFX-program which displays the simulated bodies as spheres. Unfortunately I'm struggling with memory leaks.

I figured out that the memory allocated for the spheres is not freed when the spheres are deleted from their container even if NO references (at least from my code) to the spheres exist.

It is easy to reproduce the behavior: The following code is essentially the JavaFX code generated automatically by Eclipse when a JavaFX project is created. I added a button and a group (which serves as container for the spheres). Clicking the button calls the method createSpheres which adds with each click 500 spheres to the container and which displays the used (heap) memory in the console.

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

When I start the program the used memory increases with each click although the container is cleared with container.getChildren().clear() at the beginning of the method (as expected, the call deletes the spheres of the previous button clicks BUT the used memory increases further). The following call of Runtime.getRuntime().gc() is also ineffective (as expected, the gc is started BUT the memory is not freed). It seems as if the spheres are still referenced from somewhere or if resources are not disposed, presumably within the JavaFX code.

The output of the program is shown below: I used a maximum JVM heap of 4GB (-Xmx4g). After approximately 30 clicks the maximum heap size is reached and the application crashes with an out-of-memory exception. Also shown is the Visual VM output directly before the adding of the spheres and after the exception was thrown. The latter shows an almost full "old generation" pool. The last figure shows the heap during the adding of the spheres. Although the GC performed 106 collections no memory was freed.

Program console output

Visual VM: Heap before the spheres are added

Visual VM: Heap after the spheres are added and the out-of-memory-exception was thrown

Visual VM: Heap during the adding of the spheres

It is noteworthy that the behavior depends on the radius of the spheres. In the example code each sphere has a pseudo-random radius. When the SAME radius is used for ALL spheres (independent from the value), e.g. new Sphere(100), the used memory stays constant, i.e. the memory is freed in a proper way! But the more spheres with DIFFERENT radius the more memory is consumed by the application. The following picture shows the memory when the spheres have identical radii. The memory is freed.

Visual VM: Heap during the adding of the spheres in the case of identical radii

I use JDK-9.0.1/JRE-9.0.1 and Eclipse Oxygen.1a Release (4.7.1a), Build id: 20171005-1200 and also JRE1.8.0_151 and Eclipse Neon.3 Release (4.6.3), Build id: 20170314-1500. My OS is Windows 7 Ultimate, 64-Bit.

Has anybody an idea how I can fix the problem (if possible at all) or is it actually a JavaFX memory leak issue?

1

There are 1 best solutions below

3
On BEST ANSWER

The answer to your question can be found in a package protected class named javafx.scene.shape.PredefinedMeshManager.

Every time you create an Sphere/Box/Cylinder 3D shape, the TriangleMesh object is added to a HashMap, based on a given key.

For the spheres, this key is based on their radius and number of divisions:

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

In your test, you are not modifying the number of divisions, so when you use the same radius for all the 500 spheres, you are generating the same key for all of them, so the manager hashMap will contain always one single element.

This is pretty convenient for the regular case where you have several spheres with exact same mesh: you don't have to generate all over again those meshes, you do it just once and cache the mesh.

On the contrary, if you have different radius for the spheres, the key will be always different, and on every click you will be adding 500 new objects to the hashMap.

While you clean the container with the spheres, the manager doesn't know about that and doesn't remove them from the hashMap, therefore the count increases until you get the memory exception.

With reflection, I've managed to monitor the size of the sphereCache while adding 500 spheres until reaching the memory exception:

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)

Obviously, if we had access to that cache we could prevent this memory leak:

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

}

Probably you will want to file an issue here.