Java agent that polls direct buffer pool usage every 1s and writes a TSV per sample for live monitoring of JVM direct-memory pressure on Minecraft servers.
Find a file
ekaii 4f55e9c57f
All checks were successful
release / release (push) Successful in 21s
ci: block-style env (valid YAML; flow-style ${{ }} was unparseable)
2026-07-15 12:27:56 +02:00
.forgejo/workflows ci: block-style env (valid YAML; flow-style ${{ }} was unparseable) 2026-07-15 12:27:56 +02:00
gradle/wrapper Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
src/main/java/fr/ekaii/dmagent Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
.gitignore Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
build.gradle Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
gradlew Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
gradlew.bat Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
LICENSE chore: prepare for public release 2026-07-15 01:31:48 +02:00
README.md Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00
settings.gradle Initial commit: jvm-direct-memory-agent 2026-05-05 19:59:55 +02:00

jvm-direct-memory-agent

Tiny Java agent that polls the JVM direct buffer pool every N seconds and appends a single TSV line per sample to a file. Designed for live monitoring of Netty / JDK direct-memory pressure on Minecraft (Paper/Folia) servers so an external alerting pipeline can fire before -XX:MaxDirectMemorySize is exhausted and players get kicked.

Why it exists (incident 2026-05-05)

ExtendedHorizons leaked DirectByteBuffer allocations until the configured -XX:MaxDirectMemorySize=6G was saturated. The JVM has no built-in alert for direct-memory pressure (it's not part of the regular GC heap, so heap-usage gauges miss it entirely). When it ran out, players were kicked. This agent gives us a continuously-sampled gauge — used / max — that we can scrape and alert on long before saturation.

Usage

Add to your JVM start command:

-javaagent:/path/to/jvm-direct-memory-agent-1.0.0.jar=outFile=/var/log/dmagent.tsv,intervalSec=1

Both args are optional:

key default meaning
outFile /tmp/dmagent.tsv path to append samples to
intervalSec 1 sampling interval in seconds (min 1)

The agent runs on a daemon thread, so it never holds the JVM alive on its own. The output file is opened/closed per sample (append + flush), so an external log rotator can rotate or tail -F it without coordination.

Output format

One TSV row per sample, no header:

ts_iso \t count \t used_bytes \t max_bytes \t ratio
  • ts_iso — UTC ISO-8601, e.g. 2026-05-05T17:58:22.655Z
  • count — number of live DirectByteBuffer instances (from BufferPoolMXBean#getCount)
  • used_bytes — direct memory currently allocated (BufferPoolMXBean#getMemoryUsed)
  • max_bytes — effective MaxDirectMemorySize. Resolved by parsing -XX:MaxDirectMemorySize=... out of RuntimeMXBean.getInputArguments(); if absent, falls back to Runtime.getRuntime().maxMemory() (the documented JVM default when the flag is unset). We deliberately avoid sun.misc.VM.maxDirectMemory() so the agent needs no --add-opens.
  • ratio — used_bytes / max_bytes, formatted with 6 decimals.

Example:

2026-05-05T17:58:22.655206692Z	0	0	31675383808	0.000000
2026-05-05T17:58:23.668336892Z	2	50000056	31675383808	0.001579
2026-05-05T17:58:26.670200220Z	3	150000063	31675383808	0.004736

Build

Requires JDK 21.

JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew jar

Output: build/libs/jvm-direct-memory-agent-1.0.0.jar

The jar has no runtime dependencies (everything is in java.lang.management and java.nio), so it works as a flat jar.

Smoke test

cat > /tmp/SmokeTest.java <<'EOF'
import java.nio.ByteBuffer;
public class SmokeTest {
    public static void main(String[] a) throws Exception {
        ByteBuffer b1 = ByteBuffer.allocateDirect(50_000_000);
        Thread.sleep(3500);
        ByteBuffer b2 = ByteBuffer.allocateDirect(100_000_000);
        Thread.sleep(3500);
    }
}
EOF
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
$JAVA_HOME/bin/javac /tmp/SmokeTest.java -d /tmp
$JAVA_HOME/bin/java \
  -javaagent:build/libs/jvm-direct-memory-agent-1.0.0.jar=outFile=/tmp/dmagent-smoke.tsv,intervalSec=1 \
  -cp /tmp SmokeTest
cat /tmp/dmagent-smoke.tsv

You should see ~7-8 rows, with used_bytes stepping 0 → ~50M → ~150M.