Memory Inspection (experimental)
Experimental tools that show what fills your server's memory and which plugin holds it: heap summaries, full heap dumps, and the analysis of a dump in a separate Java process.
Experimental. The commands work and are safe to use, but their output and settings may still change between versions. Heap summaries and dump analyses are written to the
memory/folder of your reports (plugins/VoxelBench/reports/memory/by default), heap dumps toplugins/VoxelBench/heapdumps/. Nothing leaves the server unless you ask for it: a summary or an analysis is sent only when you type/bench memory uploadorshare(see Sending and Sharing a Report). A heap dump never leaves the server: VoxelBench has no command, button or automatic path that sends one.
/bench memory answers "what fills my server's memory, and which plugin is holding it?". It does what spark's /spark heapsummary and /spark heapdump do, built into VoxelBench, with the classes attributed to the plugin they come from, and it can read the dump for you:
- a heap summary counts the objects of every class on the Java heap (the JVM's class histogram, what
jcmd <pid> GC.class_histogramprints), groups them by owner (each plugin, the server, the JDK) and saves the result as a small JSON file; - a heap dump writes the whole heap to an
.hproffile you open with Eclipse MAT, VisualVM or IntelliJ IDEA to follow references and find what keeps objects alive; - a dump analysis reads a heap dump in a separate, low-priority Java process β never inside the server β and tells you which plugin retains the memory, the biggest leak suspects and the chain of references that keeps each one alive: the first thing you would look for in Eclipse MAT, as a small JSON report with no value from the heap.
Quick Start
/bench memory summary live β count the live objects per class and per plugin (short freeze)
/bench memory show 1 MyPlugin β the heaviest classes of one plugin in the newest summary
/bench memory dump live analyze β what a heap dump followed by its analysis would cost, nothing written yet
/bench memory dump live analyze confirm β write it (the whole server freezes meanwhile), then analyse it
/bench memory analyze last β analyse the newest dump already on disk
/bench memory share last β a public link to a cleaned copy of the newest report, to get help
/bench memory upload last β send the newest report to your voxelbench.com account
Typed alone by a player, /bench memory summary, /bench memory dump and /bench memory analyze open a form with their options β live or all objects, gzip, analysis mode, which dump, keep, share or upload the result β and the dump form leads to the usual preview and confirmation (see Forms).
Or, without typing: /bench gui β Memory for summaries and the analysis of the last dump, Reports β Memory for the saved ones (see In the Menu).
Commands
/bench memory needs voxelbench.memory (operators only by default), on top of voxelbench.use. Everything under /bench memory dump, and starting or cancelling an analysis, also needs voxelbench.memory.dump, which voxelbench.memory does not grant. Sending a report to your account needs voxelbench.memory.upload, publishing it by public link voxelbench.memory.share: neither is granted by another node. All commands work from the console.
| Command | Description |
|---|---|
/bench memory [status] | What this Java runtime supports, heap usage, saved summaries, analyses and dumps, the analysis in progress, the analysis settings, and whether reports can be uploaded or shared |
/bench memory summary [live|all] [upload|share] | Take a heap summary. live (default): a full GC first, then only live objects are counted. all: no GC, dead objects waiting to be collected are counted too. With upload or share, the summary is sent as soon as it is saved |
/bench memory list | Saved summaries, newest first, with a number (#1 = newest); dumps too if you hold voxelbench.memory.dump |
/bench memory show <number|id|last> [owner] | Print a saved summary again; with a plugin name (or server, jvmβ¦), that owner's heaviest classes |
/bench memory delete <number|id|last> | Delete a saved summary |
/bench memory dump [live|all] [gzip] [force] [analyze [quick|full|auto]] | Show what a heap dump would cost (freeze, file size, free disk space) and what it contains; with analyze, also the memory the analysis may use. Nothing is written. Refused when the estimated freeze reaches the server watchdog's limit, unless you add force (see Mind the server watchdog) |
/bench memory dump [live|all] [gzip] [force] [analyze [quick|full|auto] [upload|share]] confirm | Write the dump previewed by the same sender less than 60 s ago, with the same options; with analyze, analyse it right after (see Analysing a Dump), and with upload or share, send that analysis once saved β never the dump |
/bench memory dump list | Heap dumps on disk |
/bench memory dump delete <number|id|last> | Delete a heap dump |
/bench memory analyze [<number|id>|last] [quick|full|auto] [upload|share] | Analyse a heap dump already on disk (the newest by default) in a separate Java process. auto (default): full if the memory allows it, else quick. With upload or share, the report is sent as soon as it is saved |
/bench memory analyze cancel | Stop the analysis in progress (its process is killed; no report) |
/bench memory analyze list | Saved analyses, newest first |
/bench memory analyze show <number|id|last> | Print a saved analysis again |
/bench memory analyze delete <number|id|last> | Delete a saved analysis |
/bench memory upload <id|#|last> [preview|confirm] (alias send) | Send ONE heap summary or dump analysis to the voxelbench.com account this server is linked to, right away; preview shows what would leave without sending, confirm sends what the preview showed (see Sending and Sharing a Report) |
/bench memory share <id|#|last> [preview|confirm] | Publish a cleaned copy of ONE report behind a public, unlisted link (no account, 7 days) |
/bench memory unshare <id|#|last> | Delete that public link at once |
The options of dump go in any order (dump all gzip force, dump live analyze quickβ¦); an analysis mode is only accepted with analyze, upload/share only with analyze (a dump alone is never sent), and confirm comes last. analyse is accepted for analyze. Only one inspection runs at a time (summary, dump, its compression or an analysis). Listing, showing and deleting analyses only need voxelbench.memory, like summaries.
Reading a Summary
Heap summary 20260924-210908-live - live objects, after a full GC (experimental)
Heap in use: 3689.8 MB before, 3223.0 MB after the GC (max 4096 MB).
Server frozen for 1947 ms (inspection 1968 ms)
Live objects: 3212.0 MB in 50987965 objects of 9635 classes.
Whose classes fill the heap (share of the bytes):
JDK: 1712.1 MB (53.3%) 2713446 objects
server: 96.7 MB (3.0%) 2294654 objects
spark: 0.0 MB (0.0%) 1471 objects
Plugins (2):
MemBallast: 1403.1 MB (43.7%) 45975201 objects
VoxelBench: 0.1 MB (0.0%) 3193 objects
Heaviest classes:
byte[] [JDK] 1433.3 MB (44.6%) 512116 objects
MemBallast$Cell [MemBallast] 1403.1 MB (43.7%) 45975200 objects
Object[] [JDK] 205.7 MB (6.4%) 270344 objects
AABB [server] 21.0 MB (0.7%) 343388 objects
...
Saved to plugins/VoxelBench/reports/memory/20260924-210908-live.json (17 KB).
(MemBallast is the test plugin used for the measurements below: it keeps 1.4 GB of small objects and 1.4 GB of byte arrays alive.)
-
Owners. Each class goes to:
- a plugin name when it comes from that plugin's jar or from a library the plugin declares (
libraries:); an array of a plugin's objects (Foo[]) and its lambdas count for the plugin too; - server for Minecraft, Paper/Spigot and the libraries the server ships. A class a plugin embeds without relocating it, but that the server also provides (Gson, fastutilβ¦), is the server's: Bukkit's class loaders ask the server first, so that is the copy in use;
- JDK for Java's own classes and every primitive array (
byte[],int[]β¦); - spark for the copy of spark bundled in Paper, shared library for a library several plugins declare, unknown when the origin cannot be found.
show <id> <owner>accepts a plugin name orserver,jvm(orJDK),library,spark,other. - a plugin name when it comes from that plugin's jar or from a library the plugin declares (
-
Sizes are shallow. Each object is counted alone, for the class that defines it, not what it keeps alive. JDK types (
byte[],String,HashMap$Nodeβ¦) are used by everyone, and a histogram cannot tell which plugin holds them: a plugin caching a million strings shows up under JDK. When most of the heap is JDK primitive arrays, the summary says so in yellow, in chat and on its sheet, and points to the analysis of a heap dump, which can tell (Analysing a Dump). In our tests, a plugin caching 500 MB ofbyte[]got 0 % in the summary, and 46 to 51 % of the heap in the full analysis of a dump. -
live or all.
livecounts what survives a full GC: it is what you compare over time to follow a leak (take a summary, wait, take another).allskips the GC, so the freeze is about half as long, but garbage not yet collected inflates the numbers, and by how much depends on when the last GC ran.
The JSON file (voxelbench-heap-summary/1) keeps the 100 heaviest classes, the 10 heaviest of each owner, the heap figures, the measured freeze and the server and Java versions.
Heap Dumps
A heap dump is the complete content of the Java heap. It is the tool to use when a summary shows a growing class and you need to know what keeps it alive.
The whole server freezes while the dump is written β every world, every region on Folia, every player β for seconds per gigabyte, depending mostly on the disk (see Measured Costs). Players may time out during a long freeze. Hence the two steps:
/bench memory dump(ordump all,dump live gzipβ¦) shows how long the server will freeze, how large the file will be, where it goes and how much disk space is free, and reminds you what the file contains. Nothing is written.- The same command followed by
confirm(or simply/bench confirm, a click on the confirmation line, or Yes in the window that opens for a player), by the same player or console, within 60 s, announces the freeze, waits one second for that message to reach the players, then writes the dump.
The estimated freeze comes from the speed of the last dump on this server; before the first one, VoxelBench assumes a slow disk (50 MB/s) and says how much faster a fast SSD would be.
Before the preview and again before writing, VoxelBench checks the free disk space: it refuses unless the estimated file size (1.5 Γ the heap in use; with gzip, half as much again for the compressed copy) plus memory-inspection.dump.min-free-disk-mb (1024 MB by default) is free. On a hosting panel, set your plan's disk limit so that this check counts it (see On a hosting panel).
live(default) runs a full GC first and dumps only reachable objects: smaller file, the usual choice.allskips the GC and keeps the garbage too.gzipcompresses the dump after it is written, in the background, while the server runs again, then replaces the.hprofby the.hprof.gz(7Γ smaller in our test). A scored run that starts meanwhile stops the compression and the.hprofis kept.- At most
memory-inspection.dump.max-filesdumps are kept (2 by default): the oldest are deleted after a new one. - The file is only readable by the account running the server:
600permissions in a700folder on Linux (HotSpot already creates dumps that way), owner-only access on Windows. - A dump interrupted by a crash leaves its partial file in
heapdumps/.work/, deleted at the next start.
Let VoxelBench analyse it (/bench memory analyze, see Analysing a Dump), or open it with Eclipse MAT, VisualVM or IntelliJ IDEA on the machine where it is, or copy it yourself over a private channel. Delete it with /bench memory dump delete <id> once analysed.
A heap dump contains everything the server has in memory: this server's voxelbench.com link token, the signing secret VoxelBench decodes to authenticate its reports, the database passwords and API keys of your plugins, player data, chat, IP addresses⦠VoxelBench never sends it anywhere and has no command to do so. Never post it publicly, never attach it to a support request, and keep
heapdumps/out of shared folders and backups that other people can read.
Mind the server watchdog
Spigot and Paper consider a server that has not ticked for settings.timeout-time seconds (spigot.yml, 60 by default) crashed: when the dump ends, the watchdog stops the server (and restarts it if restart-on-crash is on and a restart script exists). We saw it happen: a 116 s freeze on a 4 GB heap stopped Paper as soon as it resumed; the dump itself was complete and kept.
So a dump whose estimated freeze reaches that limit is refused β at the preview, at the confirmation, and again just before writing, since the heap may have grown meanwhile. The chat says why (the estimated freeze and the limit) and how to go ahead anyway:
- raise
timeout-time(read at startup, so restart once) β the safe way; or, if the server may be stopped, - add
force:/bench memory dump all forceshows the preview, with the watchdog warning repeated in red, then/bench memory dump all force confirmwrites it.
force lifts that one refusal and nothing else: the disk space check, the pause around scored runs, one inspection at a time, the confirmation within 60 s by the same sender and the voxelbench.memory.dump permission all still apply. The confirmation must carry the same options as the preview, force included.
- Between half the limit and the limit, the preview warns without refusing.
- Before the first dump on a server, the estimate assumes a slow disk (50 MB/s), so a first large dump may be refused although a fast SSD would write it in time; the preview says how fast an SSD would be. Every dump then measures the real speed for the next estimate.
- When VoxelBench cannot read the limit (a server without Spigot's configuration API), or the watchdog is disabled (
timeout-timeof 0 or less), nothing is refused and the preview says so.
Shorter freezes only make Paper print a harmless "The server has not responded⦠DO NOT REPORT THIS TO PAPER" thread dump.
Analysing a Dump
A heap dump holds the answer to "who keeps this memory alive?", but reading it takes a heap analyser and a machine with plenty of memory. /bench memory analyze does that first pass for you, on the server's machine, in a separate process, without slowing the server down:
/bench memory dump live analyze confirm β dump, then analyse it right away
/bench memory analyze β analyse the newest dump on disk (auto)
/bench memory analyze last full β insist on the full analysis
/bench memory analyze 2 quick β quick analysis of the second newest dump
Quick or full
| Quick | Full | |
|---|---|---|
| Reads the dump | once, from start to end | twice, with the whole object graph in memory |
| Per plugin | own (shallow) sizes, attributed through the real class loader of each class: exact, where a summary guesses from the jars' contents | retained sizes: what would be freed if the plugin's objects disappeared, the byte[], String and collections it holds included |
| Leak suspects | β | the biggest accumulation points, each with its dominator chain and the shortest path from a GC root (class and field names) |
| Minecraft objects | how many worlds, chunks, entities, players, block entities, item stacks⦠are in the heap | the same, split by who retains them ("LeakProbe retains: worlds 1, chunks 81, entities 200") |
| Also | duplicate strings (counts and bytes, never the text), sparse collections (ArrayList, HashMap, fastutil maps: capacity versus size) | the same, with the owner of each sparse collection |
| Memory | a few hundred MB | about 90 bytes per object of the dump in memory (3 GB for 36 million objects), about 45 with its graph on disk (1.6 GB) |
| Time, 1.4β1.6 GB dump | 4β7 s | 15β39 s |
auto (the default) runs the full analysis when it fits in memory, else the quick one; the chat and the report say which one ran and why. The quick pass always comes first: it counts the objects and references of this very dump, which gives the memory the full pass will need, and its report is kept as a fallback if the full pass cannot finish.
What one plugin retains
When you suspect one plugin, ask about it alone:
/bench memory analyze last retained MyPlugin
The analysis builds the graph of the dump, then walks it twice from the GC roots: once as it is, once without going through the plugin's own objects (its instances, the classes it defines, its class loader and those of its libraries). What only the first walk reaches is what the plugin retains β the memory that would be freed without it. The report gives that size, its share of the reachable heap, how much of it is the plugin's own objects, and the heaviest classes it holds (a cache of byte[], HashMap$Nodeβ¦).
It needs far less memory than a full analysis β no dominator tree: about 33 bytes per object with the graph in memory, or the identifiers only (8 bytes per object) with its graph on disk β and it answers for one plugin; for every plugin's retained size and the leak suspects, run the full analysis. The plugin is named as the dump knows it (from its plugin.yml), case ignored; when it is not in the dump, the report lists the plugins it knows. /bench memory analyze typed alone offers it in its form, with the list of loaded plugins.
Memory: never at the server's expense
The analysis runs in a second Java process whose heap is fixed when it starts. Before starting it, VoxelBench works out how much memory is really free:
- the machine's available memory (
MemAvailableon Linux, the free physical memory elsewhere), and, when the server runs in a container, the room left under its memory limit (cgroup v1 or v2: Docker, Pterodactyl, a systemd sliceβ¦ the tightest level counts, and its reclaimable file cache counts as free). A container limited to the server's-Xmxplus 10β20 % has no room for a 1.5 GB process: the kernel would kill the biggest process inside it, the server; - minus what the server's own heap may still grow to (its
-Xmxminus what it has reserved), minusmemory-inspection.analysis.memory-margin-mb(512 MB), minus 128 MB for the analysis process's memory outside its heap; - capped by
memory-inspection.analysis.max-heap-mb(4096 MB).
Then:
- the full analysis fits: it runs (with
autoorfull); - it does not: the quick analysis runs instead, and the chat says so before it starts ("The full analysis needs about 1538 MB and 1062 MB are available (limited by the container's memory limit): the QUICK analysis runs instead"). When the dump has never been analysed, the analysis process decides after its quick pass, from the exact number of objects;
- not even the quick one fits: the analysis is refused, with what it needs and what is available ("even the quick analysis needs about 323 MB and only 262 MB are available (limited by the container's memory limit)"). Free some memory, or copy the dump to another machine. When the container's limit is what stops it, the chat also says how much of it the server's heap may take and which
-Xmxwould leave room (see On a hosting panel); - the free memory cannot be read:
autostays quick;full, asked explicitly, runs withinmax-heap-mband says so.
The memory of the full pass is estimated from the quick pass (objects, references and what the quick pass keeps) with a 15 % margin. In our tests the real need stayed within it (on a 2.2 GB dump of 36 million objects: estimated at 3048 MB, completed with a 2700 MB heap).
When the full analysis does not fit in memory: its graph on disk
The full analysis builds a graph of every object and every reference; that graph, not the size of the file, is what takes memory. When it does not fit, the analysis process can keep part or all of it in work files instead of giving up on the full analysis (memory-inspection.analysis.disk, on by default). It picks the fastest arrangement that fits, after its quick pass:
| Arrangement | Memory (36 M objects, 75 M references) | Work files | Time, 2.2 GB dump |
|---|---|---|---|
| in memory | about 3 GB (90 bytes per object) | none | 26 s |
| identifiers, positions and tree columns on disk | about 1.9 GB | about 1 GB | 33 s |
| the whole graph on disk (references sorted in chunks, then merged) | about 1.6 GB (45 bytes per object) | about 1.8 GB | 60 s |
| everything on disk from the start, only what each step reads at random in memory | about 0.8 GB (22 bytes per object) | about 3.4 GB | 2.5 min |
The result is exactly the same in all three β retained sizes, owners, leak suspects and their paths; only the time changes. The work files go to plugins/VoxelBench/heapdumps/.work/ (readable by the server's account only), hold object numbers and positions β no value from the heap β and are removed at the end, or at the next start after a crash. The disk must keep memory-inspection.dump.min-free-disk-mb free on top of them; when it cannot, the quick analysis runs instead and says how much space was missing.
On disk, speed depends on the storage: the times above come from a hard disk helped by the system's file cache; on network storage with little free memory for that cache, expect several times longer β memory-inspection.analysis.timeout-seconds (1800 s by default) stops it, keeping the quick report. For a 20 GB heap (about 250 million objects), count about 23 GB in memory, about 11 GB with the graph on disk (15 GB of work files), or about 5.5 GB with everything on disk (about 24 GB of work files).
How the analysis process runs
- It is the server's own
java, started with the VoxelBench jar as class path and a dedicated entry class, kept intact by the obfuscation: nothing to download, no other file. - Its heap comes from the memory check above; it uses the serial GC and one compute thread (
-XX:ActiveProcessorCount=1), stops on its first out-of-memory error, and inherits none of the server's JVM options (JAVA_TOOL_OPTIONSand the like are removed from its environment). - Lowest priority:
nice -n 19plusionice -c 3(idle disk priority) on Linux when they are available, the Idle priority class on Windows (set right after the start),niceon macOS. The chat shows the priority it got. - It reads the dump and writes one JSON file. It opens no network connection and loads no server class. VoxelBench relays its progress in chat (reading the dump, building the object graph, computing retained sizes, looking for leak suspects).
- It is stopped by
/bench memory analyze cancel, when VoxelBench is disabled or the server stops, when a scored run starts, and aftermemory-inspection.analysis.timeout-seconds(1800 s; the quick report is kept when there is one). If the server crashes or is killed, the process notices within a second and exits on its own. - It never starts on its own: only through the command, a dump confirmed with
analyze, or the menu button. It is prepared and watched off the server thread.
On a machine with 2 CPU threads or fewer, the chat warns before a full analysis: even at the lowest priority it competes with the server for the CPU and its caches. With the server pinned to 2 logical CPUs we measured 17 TPS on average, and ticks up to 0.7 s for about 15 seconds while retained sizes were computed. Prefer the quick analysis there, or analyse the dump on another machine. In a container with a CPU quota, the analysis slows itself down instead (next section).
On a hosting panel (Pterodactyl, Pelicanβ¦)
The analysis process runs inside the server's container, like the server: no special permission is needed, but it shares the container's memory limit, CPU quota and disk limit with the server. VoxelBench accounts for all three.
Memory. On Pterodactyl the container's limit is your plan's memory plus 5 % (10 % under 4 GB, 15 % under 2 GB), and the default Paper startup command lets the server's heap take 95 % of it (-XX:MaxRAMPercentage=95.0). On an 8 GB plan: an 8602 MB limit, of which the server's heap may take 8172 MB. The rest barely covers the server's own memory outside its heap, so the analysis is refused, even the quick one, rather than get the server killed. The chat then says why and what would work:
The server's heap may grow to 8172 MB, 95% of the container's 8602 MB: little is left for anything else. Started with -Xmx6912M (in the server's startup command), or on a plan with more memory, the server would leave room for this analysis.
The suggested -Xmx counts the server's heap full to that limit, its memory outside the heap (as measured, at least 512 MB), memory-margin-mb and the analysis process. When even a 1 GB server heap would not leave enough room, the chat says to analyse the dump on another machine. Lowering the heap is a trade-off: restart with it only if the server's memory use leaves the room (a summary shows how much heap it really uses).
Disk. A panel measures the server folder itself and stops the server when it goes over the plan's disk limit ("Server is exceeding the assigned disk space limit, stopping process now."), while the disk seen from inside the container is the whole machine's. Set memory-inspection.host-disk-limit-gb to your plan's limit: the free space of a dump, of a decompression and of the analysis' work files is then the smallest of the disk and that limit minus what the server folder already takes (measured by walking it, kept 30 seconds). The dump preview shows it ("the server folder already takes 18.0 GB of 20.0 GB"). When VoxelBench sees a Pterodactyl or Pelican panel and the key is not set, the dump preview, and the analysis when it writes work files, warn you instead of refusing.
CPU. A plan's CPU limit is a quota for the whole container (100 % = one core). Low priority only decides between two threads waiting for the same core: what the analysis uses on another core still comes out of the quota, and when the quota runs out the kernel pauses the whole container, server included, until the next 100 ms period. So when the container's quota is memory-inspection.analysis.cpu-throttle.max-cores cores or less (2 by default), the analysis process takes only cpu-throttle.percent of one core (30 % by default), working then pausing, and its timeout is stretched as much (1800 s becomes 100 minutes). The chat says so when it starts, and the report records it (process.cpuPercent). Measured on a 296 MB dump: 8.3 s unthrottled (one core and a half, compiler threads included), 36 s at 30 % (34 % of one core on average, JVM start and report writing included), same report.
Compressed dumps
The analysis jumps around in the file, which a gzip stream cannot do. So:
dump β¦ gzip analyze confirmanalyses the.hprofbefore compressing it (the file is still in the disk cache, and no extra disk space is needed), then compresses it as usual;analyzeon a.hprof.gzalready on disk first decompresses it intoheapdumps/.work/(owner-only permissions, deleted afterwards), after checking the disk space.
Reading an Analysis
A full analysis of a 1.6 GB dump (the test plugin LeakProbe keeps a 500 MB cache in a static field, plus a world, 81 chunks and 200 entities):
Heap dump analysis 20260925-003149-full - full: retained sizes and leak suspects (requested: full, experimental)
Dump 20260925-002417-live (1630.5 MB, 13396831 objects), analysed in 15.5 s by a separate process (up to 2899 MB, low priority (nice))
Who retains the heap (what would be freed without them):
server: 649.7 MB retained (51.3%) own objects 273.6 MB
JDK: 31.6 MB retained (2.5%) own objects 996.9 MB
Plugins (2):
LeakProbe: 577.7 MB retained (45.6%) own objects 0.2 MB
VoxelBench: 7.5 MB retained (0.6%) own objects 0.2 MB
Leak suspects:
#1 HashMap$Node[] retains 500.9 MB (39.6%) [LeakProbe]
β¦ βΊ [*] βΊ <class> βΊ static CACHE βΊ HashMap.table β HashMap$Node[]
#2 ConcurrentLong2ReferenceChainedHashTable$TableEntry[] retains 141.3 MB (11.2%) [server]
β¦ βΊ ServerLevel.chunkTaskScheduler βΊ ChunkTaskScheduler.chunkHolderManager βΊ β¦ β ConcurrentLong2ReferenceChainedHashTable$TableEntry[]
LeakProbe retains: worlds 1, chunks 81, entities 200
Duplicate strings: 40608 values copied 391804 extra times, 24.7 MB wasted (counts only, never their content)
Sparse collections: 8704, 36.9 MB of empty slots (worst: ArrayList 1/4096 Γ1835 [LeakProbe])
Saved to plugins/VoxelBench/reports/memory/20260925-003149-full.analysis.json (33 KB).
- Retained: what would be freed if the object disappeared. An object belongs to the closest plugin that dominates it, the plugin every path from a GC root to it goes through. An object the server can also reach without going through a plugin (a world the server still lists) stays the server's: a plugin is never charged for what the server keeps too.
- Own objects: the shallow size of the owner's classes, as in a summary. LeakProbe's own objects weigh 0.2 MB, yet it retains 578 MB of JDK arrays: that is what a summary cannot see.
- Leak suspects: the objects retaining at least 10 % of the heap (and 1 MB), followed down to where the memory accumulates. Under each one, the shortest chain of references from a GC root, with field names (
static CACHE,HashMap.table);[*]is an array element,<class>an object's class. A plugin retaining at least 5 % of the heap (and 16 MB) also gets its own suspect. The JSON file adds the dominator chain and the heaviest children of each suspect. - Not a leak: the static state of the server's classes. On a small heap, the list of the thousands of classes the server has loaded, with everything their static fields hold (registries, mappingsβ¦), easily retains 10 % of it. It is shown apart ("Not a leak: the static state of 7412 classes (owner: server), 33.1 MB") instead of as a suspect: that size is the server's own. Compare it from one analysis to the next rather than reading it alone. VoxelBench recognises it from the shape of the heap, not from the name of a class loader, so it works the same on Spigot, Paper, Folia and their forks. A single class that retains a suspect's share on its own (a static cache that keeps growing) is still a suspect, and a plugin's classes are never put aside.
- Duplicate strings: in a full analysis, only the copies still in use count ("Duplicate strings still in use"): the garbage of an
alldump no longer inflates them. A quick analysis counts every copy in the dump.
Plugins are recognised by their class loader: the Bukkit and Paper ones, their subclasses, and any other loader that defines a plugin's main class (a subclass of JavaPlugin) β so a fork or a hybrid server with its own plugin loader is attributed the same way.
A quick analysis lists the own sizes per real class loader instead ("byte[], String⦠count for the JDK"), how many Minecraft objects the heap holds (not who holds them), duplicate strings and sparse collections, then points to the full analysis.
/bench memory analyze show <id> prints a saved analysis again. Reports (voxelbench-heap-analysis/1, 20 to 35 KB) are saved in the memory/ folder of the reports, next to the summaries, within memory-inspection.analysis.max-files and max-total-mb.
What an analysis report never contains: no string or array content, no field value (except the capacity and size of collections), no fingerprint of any content, no object address, no thread name. Only class, field and plugin names, sizes and counts. Duplicate strings are found by comparing 64-bit fingerprints inside the analysis process; the fingerprints are never written. Checked with twelve planted secrets (tokens, passwords, player names, IP addresses) plus the link token and the RCON password: all in the dump, none in the reports.
Sending and Sharing a Report
A heap summary or a dump analysis can leave the server in two ways, exactly like a profile (Profiling): to your voxelbench.com account (upload), or behind a public link to a cleaned copy (share), typically to paste in a Discord channel to get help. Both are always manual: nothing is sent without the command, never automatically, never during a scored run or the 30 s after it.
A heap dump is never sent, by any route. Only the two reports can leave: they hold class, field and plugin names, sizes and counts, never a value from the heap.
/bench memory upload <id>.hprofanswers that a heap dump never leaves the machine;/bench memory dump live uploadis refused (addanalyze: the analysis is what gets sent). The sending code only knows the reports folder, and refuses anything that is not a report (an.hprofrenamed.jsonincluded).
Which report. <id|#|last> designates a summary or an analysis: its id (tab completion lists them), a unique prefix of it, last (the newest of both β what you just produced), or #N (rank among summaries and analyses together, newest first, as in Reports β Memory). The chat always names the kind and the id of the report it sends.
Right after producing it. Add the verb to the command that creates the report, and it leaves as soon as it is saved:
/bench memory summary live share β a summary, then a public link to its cleaned copy
/bench memory analyze last upload β analyse the newest dump, send the report to your account
/bench memory dump live analyze upload β preview; then β¦ confirm: dump, analysis, and the analysis sent
The permission, the setting, the linked server and the pause around scored runs are checked before the summary freezes the server or the analysis starts: a refusal never costs you a freeze for nothing.
Upload to your account
- Link the server first:
/bench link(see Account Linking). /bench memory upload <id|#|last>sends the report right away. If a class, field or plugin name looks like an address, a path, a script name or a generated identifier, the chat says so first: an upload sends the file as it is, to your account only.- To look first, add
preview: it shows where the report goes, its size, the plugin names, how many class (and field) names it contains, the platform β and sends nothing./bench memory upload <id> confirm(or/bench confirm, a click on that line, or Yes in the window that opens) then sends exactly that, within 60 s, from the same player or console, if the file has not changed.
/bench memory list then marks the summary sent (/bench memory analyze list for an analysis) and show prints the link to its page on voxelbench.com. What is sent: exactly the JSON file of the report, compressed. /bench memory delete removes the local copy only and reminds you that the uploaded copy stays on voxelbench.com.
Share by public link
/bench memory share <id|#|last> publishes a cleaned copy of the report behind a link that anyone who has it can open, unlisted, with no account and no linked server, that expires after 7 days. preview shows first what would be published (and says in so many words that the link is public), confirm publishes exactly that copy. /bench memory unshare <id|#|last> deletes the link at once β even after the report was deleted locally (the deletion token is kept in <id>.share.json, or <id>.analysis.share.json, until the link expires) and even with sharing disabled.
What the public copy keeps: plugin names (private ones included β they are what makes the report useful to whoever helps you; masking chosen plugins as plugin#N will come later), class and field names, sizes, shares, the leak suspects and their path, duplicate strings and sparse collections counts, Minecraft object counts, the server and Java versions, OS, CPU count and GC names.
What it removes or coarsens:
- every field VoxelBench does not know to be safe, and the support notes (
diagnostics,warnings): a field added by a future version is not published until reviewed; - the time zone: the date is rounded to the hour in UTC, the report id is rebuilt from it, the dump's local id and date are removed;
- the machine's memory: free RAM, container limit and headroom are removed (only the decision taken β quick or full, limited by what β stays); the server's heap sizes, the dump size and the analysis process's memory are rounded to 64 MB;
- in class, field and plugin names, replaced by a marker such as
[ip]: IP addresses (also written10_0_0_5), URLs, e-mail addresses, file paths, UUIDs, long hexadecimal strings, and the names this server knows β online players, worlds (except the default ones), the OS user and machine name, the server folder, the server id and the link token β outside the standard namespaces (a player named "level" does not scramble Minecraft'sβ¦world.level.Level); - generated classes: a class named after a script (Rhino, Nashorn, Kotlin or Groovy scripts) becomes
[script]; proxy suffixes ($$EnhancerByCGLIB$$β¦,$ByteBuddy$β¦,$Proxy12) lose their generated part.
The chat says how many names were cleaned and with which markers. The cleaning recognises patterns and the names above; it cannot guess an offline player's name glued inside a class or field name β check the preview when in doubt. Only official VoxelBench builds can share (voxelbench.com checks the jar's signature, as for benchmark reports); a development or modified build is told so, and upload still works with a linked server. The site receives this server's id only to limit abuse and never shows it.
When it is refused
memory-inspection.upload.enabled/share.enabledisfalse, or you lackvoxelbench.memory.upload/voxelbench.memory.share(operators have them; no other node grants them);- upload only: the server is not linked;
- a scored run is in progress, or ended less than 30 s ago;
- the report (for a share, its cleaned copy) is larger than
max-size-kb(1024 KB by default); - another send is in progress, the previous one was less than 30 s ago, or voxelbench.com asked to wait (
Retry-After); - share only: the report already has a live public link (
unshareit first).
Until voxelbench.com accepts memory reports, the answer is "voxelbench.com cannot receive (share) memory reports yet": nothing is stored, nothing to fix on your side.
In the Menu
/bench gui β Memory (main menu, under the Profiler): Summary: live objects and Summary: all objects, each showing the expected freeze and asking for a confirmation, since a summary freezes the server; Analyse the last heap dump, which asks for a confirmation (the analysis process may use up to max-heap-mb of memory and one CPU core, at low priority) then runs /bench memory analyze last auto β greyed out without voxelbench.memory.dump, without a dump on disk, or when analyses are disabled; Cancel the analysis while one runs; Full status in chat (/bench memory status); the current state (heap in use, inspection or analysis running and its phase, pause around a scored run, saved summaries and analyses); and Saved summaries and analyses.
/bench gui β Reports β Memory lists the saved heap summaries and dump analyses, newest first: date, mode, heap in use and measured freeze for a summary, analysed dump and number of leak suspects for an analysis, heaviest plugin. Clicking a summary opens its sheet:
- a header with the id, date, mode, server and Java versions, heap before and after the GC, measured freeze, totals and file;
- the owners, heaviest first β plugins, server, JDKβ¦ β with their size, share, objects and classes; click one to show its heaviest classes below (the heaviest plugin is selected when the sheet opens);
- the heaviest classes of the whole summary, and how to read the numbers β including, when JDK arrays dominate, that the summary cannot say who holds them and that a dump analysis can;
- the buttons Show in chat (
/bench memory show <id>), <owner> in chat (/bench memory show <id> <owner>, full class names), New summary (same mode, to compare; asks for a confirmation) and Delete (asks for a confirmation).
Clicking an analysis opens its own sheet: a header (mode run and mode asked, why a quick one ran instead, the dump, how long it took and with how much memory); the owners by retained size (own sizes for a quick analysis), a click selecting one; the selected owner's detail (retained and own sizes, the Minecraft objects it retains, its suspects); the leak suspects with their path; duplicate strings, sparse collections and Minecraft objects; how to read it; Show in chat (/bench memory analyze show <id>) and Delete (the report only, the dump is not touched; asks for a confirmation).
Both sheets also show Upload and public link (sent onβ¦, public link valid untilβ¦; a click prints the links in the chat) and the buttons Preview (what a public link would publish; shift-click: what an upload would send; nothing leaves), Send to voxelbench.com and Share by public link (each asks for a confirmation), and Delete the public link while one is live β greyed out without voxelbench.memory.upload / voxelbench.memory.share or when disabled in config.yml. The list marks reports Sent to voxelbench.com and Public link until β¦.
Every button runs the matching /bench memory command, so permissions, refusals and messages are exactly those of the command; the screens need voxelbench.memory. Heap dumps have no button and never appear in the menus: they stay command-only, with their preview and confirmation (typed alone, /bench memory dump opens a form that only fills in its options, then shows the same preview); the menu can only analyse one that is already on disk, and only a report can be sent. The reports Cleanup button never touches memory/: summaries and analyses keep their own retention (memory-inspection.summary, memory-inspection.analysis). The web dashboard has no memory section.
Measured Costs
Measured on Paper 1.21.11, Java 21 (G1), Windows 11, 12 CPU threads, with a test plugin keeping small objects and byte arrays alive (21 million objects in total with a 2 GB heap, 51 million with 4 GB). Your figures depend on the number of objects, the CPU and above all the disk.
| Heap (-Xmx / in use) | Operation | Freeze | File |
|---|---|---|---|
| 2 GB / 1.4 GB | summary live | 1.0 β 1.1 s | 17 KB JSON |
| 2 GB / 1.4 GB | summary all | 0.4 β 0.6 s | 17 KB JSON |
| 2 GB / 1.4 GB | dump live | 27 s | 1.77 GB .hprof |
| 2 GB / 1.4 GB | dump all | 19 β 25 s | 1.79 β 1.84 GB .hprof |
| 2 GB / 1.4 GB | dump live gzip / all gzip | 19 β 42 s, then 26 β 39 s of compression with the server running | 248 β 251 MB .hprof.gz |
| 4 GB / 3.7 GB | summary live | 1.9 s | 17 KB JSON |
| 4 GB / 3.3 GB | summary all | 1.0 s | 17 KB JSON |
| 4 GB / 3.3 GB | dump live | 53 s | 4.27 GB .hprof |
| 4 GB / 3.3 GB | dump all | 116 s β Paper's watchdog (60 s) then stopped the server | 4.34 GB .hprof |
| Folia 26.1.2, Java 25: 1 GB / 0.8 GB | summary live / all | 0.58 s / 0.39 s | 17 KB JSON |
| Folia 26.1.2, Java 25: 1 GB / 0.5 GB | dump live | 0.9 s (dump call 3.1 s) | 638 MB .hprof |
Recent JVMs write the dump in parallel during the freeze and finish the file after the server has resumed, hence a freeze much shorter than the dump call on Java 25. The test disk wrote about 50 MB/s, so the dumps were disk-bound and varied with its load: a fast SSD cuts the freeze several times. A .hprof weighs 1.3 to 1.4 times the heap in use (small objects cost more in the file than in memory). The freeze is measured by VoxelBench itself (the longest stall seen by a thread that wakes up every millisecond) and shown after each operation; the next preview uses the speed of the last dump to estimate it.
Dump analyses (Paper 1.21.11, Java 21, server -Xmx2G, a test plugin holding a 500 MB cache; the server kept running with no player):
| Machine | Dump | Analysis | Time | Analysis process | Server meanwhile (TPS, average MSPT) |
|---|---|---|---|---|---|
| Windows 11, 12 CPU threads | 1.43 GB, 10.5 million objects | quick | 6.8 s | 256 MB heap | TPS 20.0 |
| Windows 11, 12 CPU threads | same | full (needs 1.14 GB) | 30 β 39 s | up to 4 GB heap allowed, Idle priority | TPS 20.0, worst 1-second window 19.6; MSPT 18 β 19 ms (16 ms before) |
Docker eclipse-temurin:21, 6 GB limit, cgroup v2 | 1.63 GB, 13.4 million objects | full (needs 1.41 GB) | 15.5 s (20 s right after the dump) | peak resident memory 1.45 GB, nice 19 + idle I/O | TPS 20.0, MSPT 11 ms (15 ms before) |
| same, 4.2 GB limit | same | auto β quick (full needed 1.54 GB, 1.06 GB free under the limit) | 4.0 s | 256 MB heap | β |
| same, 3.4 GB limit | same | refused (quick needs 323 MB, 262 MB free under the limit) | β | not started | β |
| Windows, server pinned to 2 logical CPUs | 1.43 GB | full | β | Idle priority | TPS 17.2 on average, worst 1-second window 6.6, ticks up to 0.7 s: hence the warning on small machines |
An earlier version of the parser took 122 s for the same full analysis; the figures above are the current ones. The analysis time grows with the number of objects rather than the file size.
Scored Runs
Like the profiler, memory inspection never runs during a scored run: summaries, dumps and analyses are refused while a benchmark, stress limit, tier or auto-bench run is in progress, or a test whose result is sent to voxelbench.com, and during the 30 s after it ends. A full GC or a frozen server would distort the score, and an analysis would take CPU time from it. A scored run that starts during an analysis stops it (its process is killed, no report).
Requirements and Limits
- A HotSpot JVM (Temurin, Oracle, Zulu, Correttoβ¦, Java 16 to 25). VoxelBench uses the JVM's diagnostic MBeans (
com.sun.management:type=DiagnosticCommandfor the histogram,HotSpotDiagnosticfor the dump), without any extra download. - OpenJ9 / IBM Semeru is not supported: it has no class histogram MBean, and its
dumpHeapis not implemented (checked on Semeru 21)./bench memory statussays so; use OpenJ9's ownjcmd <pid> GC.class_histogramandjcmd <pid> Dump.heapinstead. A minimaljlinkruntime without thejdk.managementmodule is reported the same way. Nothing else in VoxelBench is affected. - The freeze is JVM-wide. On Folia too, every region stops.
- Attribution by class name. A histogram names classes but not their class loader: when two plugins embed the same class under the same name, the first plugin's jar wins, like in the profiler.
- Memory. A summary reads the histogram as text (a few MB on a large server) and the jar indexes of your plugins, off the tick thread. A dump needs no extra heap. An analysis needs no heap from the server: its separate process needs a few hundred MB (quick) or about 90 bytes per dumped object (full; about 45 with its graph on disk), checked against the free memory and the container limit before it starts.
- Analysis limits. Plugins are recognised through Bukkit's and Paper's plugin class loaders; the classes of a plugin loaded some other way count as
other. Dumps of heaps above 32 GB (uncompressed references) are handled but untested, and a dump with more than about two billion objects or references gets the quick analysis only. Tested on Paper 1.21.11 with Java 21 (Windows and Linux in Docker); Spigot and Folia dumps use the same format but were not part of the tests. The dump file must come from a HotSpot JVM (which is what/bench memory dumpwrites).
Privacy
A heap summary contains class names β which reveal your installed plugins, private ones included β plugin names, heap sizes, the measured freeze, your server and Java versions, OS and CPU count. It holds no value from the objects themselves: no player name, no UUID, no address, no chat, no configuration. It stays in your reports folder unless you send or share it.
A dump analysis contains the same kind of data, plus field names (along the paths of the leak suspects), class loader types, the Minecraft object counts and the memory figures of the machine (free memory, container limit). It holds no value from the heap (see Reading an Analysis). It stays in your reports folder unless you send or share it.
/bench memory upload sends one report as it is to your voxelbench.com account; /bench memory share publishes a cleaned copy behind a public link for 7 days β without the time zone, the machine's memory figures, addresses, paths and the names your server knows (see Share by public link). Both only on your command, one report at a time, never during a scored run.
A heap dump contains everything in memory (see the warning above). It stays in plugins/VoxelBench/heapdumps/, outside the reports folder, which is often shared for support, and it never leaves the server: no command, button, automatic path, report or monitoring payload sends it.
No benchmark report, monitoring payload or profile carries memory inspection data.
Configuration
memory-inspection:
enabled: true
summary:
max-files: 20
max-total-mb: 20
host-disk-limit-gb: 0
dump:
enabled: true
max-files: 2
min-free-disk-mb: 1024
analysis:
enabled: true
max-heap-mb: 4096
disk: true
memory-margin-mb: 512
timeout-seconds: 1800
cpu-throttle:
max-cores: 2
percent: 30
max-files: 20
max-total-mb: 20
upload:
enabled: true
max-size-kb: 1024
share:
enabled: true
max-size-kb: 1024
See Configuration - Memory Inspection for each key and its bounds.