Where the memory actually goes
Run spark --heap on a server like this and the picture is always the same: one Map<UUID, Profile> loaded at join, holding every heart transaction the server ever saw, never evicted — plus a second copy keyed by name for tab lookups. Sixty concurrent players, four gigabytes, and an owner convinced "plugins just need RAM".
They don't. They need an eviction policy.
The N+1 on every kill
The kill path is where lifesteal plugins go to die. Per heart, per kill: a SELECT for the victim, an UPDATE for the winner, an INSERT for the log — sometimes three of them on the main thread, synchronously, while forty players fight in an event. Ten players trading hearts in one fight is a hundred round-trips the tick never sees.
The fix, in order
- Repository layer. One interface — load, save, query. Every SQL lives behind it, and swapping databases stops being a rewrite.
- Evict on quit. Save flush, drop the entry. Nothing personal lives in memory after the player leaves.
- Cap the cache. LRU at a few hundred entries beats a map that grows until the GC gives up. The hot players stay warm; everyone else costs nothing.
- Write behind, in order. One executor, a bounded queue, batched transactions. Kills in a fight collapse into one write, off the tick thread.
- Log tables get seasons. Partition or prune at reset — the log is history, not state.
Same plugin, same sixty players, same fights: about 380 MB steady after the rewrite. The four gigabytes were never the price of the genre.
spark — memory and heap profiling — read on September 2026
RnK is a Minecraft developer with six years in the game — plugins, server setups and performance work across Paper, Spigot and Folia backends. Three networks run day to day; the rest of the time is client work. The bench is open on Discord.