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.
