1

I'm developing a game in java using the LWJGL library, and I came across a huge memory leak after creating an infinite terrain generator. Basically, the terrain is divided into chunks and I have a render distance variable like 8. I have a HashMap which maps every created Terrain with it's coordinate, represented by a custom Key class. Then, for each Key (coordinates) within the render distance, I'm checking if the HashMap contains this Key, if it doesn't I'm creating it and adding it to a List of terrains to render. Then, I render the terrains, clear the list, and every 5 seconds I check every key of the Hashmap to see if it is still in the render distance and remove the ones that don't.

The thing is that I have 2 memory leak, I mean that memory is increasing with time. To debug my code, I simply remove each part until the memory stops increasing. I found 2 parts causing the 2 leaks:

First, the HashMap is not clearing correctly the references to the Terrains. Indeed, I kept track of the size of the HashMap and it never goes beyond ~200 elements, even though the memory increases really fast when generating new terrains. I think I'm missing something about HashMap, I have to precise that I implemented a hashCode and equals methods to both Key and Terrain class.

Second, creating new Keys and storing a reference to the Terrain in my loops to avoid getting it in the HashMap every time I need it are causing a small but noticeable memory leak.

RefreshChunks is called every frame, and deleteUnusedChunks every 5 seconds

public void refreshChunks(MasterRenderer renderer,Loader loader, Vector3f cameraPosition) {
        
        int camposX = Math.round(cameraPosition.x/(float)Terrain.CHUNK_SIZE);
        int camposZ = Math.round(cameraPosition.z/(float)Terrain.CHUNK_SIZE);
        campos = new Vector2f(camposX,camposZ);

        for(int x = -chunkViewDist + camposX; x <= chunkViewDist + camposX; x++) {
            for(int z = -chunkViewDist + camposZ; z <= chunkViewDist + camposZ; z++) {
                Key key = new Key(x,z); //Memory increases
                Terrain value = terrains.get(key); //Memory increases
                if(Maths.compareDist(new Vector2f(x,z), campos, chunkViewDist)) {
                    
                    if(value == null) {

                        terrains.put(key, new Terrain(loader,x,z,terrainInfo)); //Memory leak happens if I fill the HashMap
                    }
                    if(!terrainsToRender.contains(value)) {
                        terrainsToRender.add(value);

                    }
                } else {
                    if(terrainsToRender.contains(value)) {
                        terrainsToRender.remove(value);

                    }
                }
            }
        }
        renderer.processTerrain(terrainsToRender);
        
        if(DisplayManager.getCurrentTime() - lastClean > cleanGap) {
            lastClean = DisplayManager.getCurrentTime();
            deleteUnusedChunks();
        }
    }
    
    public void deleteUnusedChunks() {
        List<Key> toRemove = new ArrayList<Key>();
        terrains.forEach((k, v) -> {
            if(!Maths.compareDist(new Vector2f(k.x,k.y), campos, chunkViewDist)){
                toRemove.add(k);
            }
        });
        for(Key s:toRemove) {
            terrains.remove(s);
        }       
    }

Key class implementation:

public static class Key {

        public final int x;
        public final int y;

        public Key(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Key)) return false;
            Key key = (Key) o;
            return x == key.x && y == key.y;
        }

        @Override
        public int hashCode() {
            int result = x;
            result = 31 * result + y;
            return result;
        }

    }

Terrain class hashCode and equals implementation:

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Terrain)) return false;
        Terrain te = (Terrain) o;
        return gridx == te.gridx && gridz == te.gridz;
    }

    @Override
    public int hashCode() {
        int result = gridx;
        result = 31 * result + gridz;
        return result;
    }

I'm certainly missing something about the behavior of HashMaps and classes, thanks for your help.

Edit:

My questions are: • Is it normal that instantiating a new key class and create a reference to a terrain are making the memory increase over time?

• Why while the HashMap size stays the same when putting new elements and removing old ones the memory keeps increasing over time?

Edit 2: I tried generating terrains for about 10min, and at some point, the program used 12 GB of ram, but even though I couldn't get it to crash with a out of memory exception because the more I was generating, the less ram was added. But still I don't my game to use the maximum amount of ram available before starting to recycle it.

Thibault Abry
  • 156
  • 10
  • What is you question? You want us to find memory leak? Or teach you how to do it yourself? Former is extremely hard if even possible with info you gave, later is offtopic here. – talex Aug 28 '20 at 10:56
  • it's normal for memory use to increase while the application runs. Does the application eventually crash with "out of memory error"? – Joni Aug 28 '20 at 10:56
  • @talex I'm editing my post to clearly ask my question. – Thibault Abry Aug 28 '20 at 11:21
  • @Joni No but at some point it would have crash, the program gained like 50mo of ram per seconds – Thibault Abry Aug 28 '20 at 11:23
  • 1
    The Java virtual machine has a garbage collector. It keeps track of what objects are in use and what objects are not. It will eventually delete unused objects, so that you don't run out of memory. So far nothing here indicates you have a memory leak - only an "out of memory error" will tell you if you actually have a memory leak. – Joni Aug 28 '20 at 11:25
  • 1
    @ThibaultAbry: It's really important how you measure the memory increase. If all you do is check in your task manager, then that's not a good indication of a memory leak. Due to the garbage collector it's possible (and normal) for a Java program to use more memory than it would strictly "need" (i.e. having some spare makes garbage collection much easier). Try using real Java-specific tools too take memory dumps and check those for memory leaks. – Joachim Sauer Aug 28 '20 at 11:34
  • @JoachimSauer Okay I'll try to use a Java-specific tool, but I don't think that's normal behavior for the program to use12gB of ram after 10min of terrain generation when it used ~400mB on start. I'll edit my original post to precise that – Thibault Abry Aug 28 '20 at 11:47
  • 1
    If you think 12GB is too much memory, don't let the JVM use that much. Use the JVM startup options to give it less memory. https://stackoverflow.com/questions/1565388/increase-heap-size-in-java – Joni Aug 28 '20 at 18:54

0 Answers0