Should You Upgrade Your Minecraft Server from Java 21 to Java 26? Here's my Answer

Every six months, like clockwork, the same question resurfaces in every Minecraft server admin Discord and every r/admincraft thread: "New Java version dropped, should I upgrade?"

And every six months, the same two camps form. One side says "just stick with LTS, don't touch what works." The other side posts screenshots of someone's personal benchmark running a single-player world and declares Java 26 is 40% faster. Nobody actually digs into the specifics. Nobody maps the JVM engineering work onto what a Minecraft server actually does. The conversation dies, everyone stays on Java 21, and we do it again in six months.

This is that article, but done properly.

I want to actually answer whether you should upgrade your Minecraft server from Java 21 — which is what almost everyone is running — to Java 22, 23, 24, 25, or go all the way to Java 26. That means understanding what each version actually changed in the JVM, mapping those changes to what a Minecraft server actually does with the JVM, and giving you a real decision framework at the end that isn't just "it depends."

Spoiler: it does depend. But on specific, concrete things we can reason about.

What Actually Makes a Minecraft Server Slow?

Before we talk Java versions, we need to establish what a Minecraft server actually puts the JVM through. Because "is Java X faster than Java Y" is meaningless unless you understand what kind of work is being done.

A Minecraft server runs one main tick loop at a target of 20 ticks per second. Each tick has a 50 millisecond budget. Inside that 50ms, the server needs to: process all player movement and input, run all entity AI pathfinding, update redstone contraptions, handle block updates, run mob spawning logic, advance scheduled tasks, execute any synchronous plugin code, and then send the results to every connected client. Every tick. 20 times per second. With no exceptions.

When you can't finish all of that within 50ms, your TPS — ticks per second — drops below 20. Players feel this as rubber-banding, delayed block placement, inventory lag, and the general feeling that the server is "heavy." When TPS drops to 15 you're missing 25% of your ticks. Below 10 and the game is genuinely unplayable.

The JVM contributes to lag in three main ways:

Garbage Collection Pauses. Java is a garbage-collected language. Memory that your program allocates and stops using gets cleaned up by the garbage collector. The problem is that some collection work requires the JVM to pause all running threads — including your server's main tick thread. When this happens, your server just freezes. For however long the GC runs. This is called a stop-the-world pause. A 200ms GC pause is four missing ticks. Players see rubber-banding. This is the single biggest JVM-related source of Minecraft server lag, and it's the main reason which GC you run and how you configure it matters enormously.

To understand the scale of the problem: Aikar's research — the guy whose G1GC flags the entire community has been running for years — documented that a 30-player Minecraft server generates at least 800 megabytes of heap allocations per second. Not per minute. Per second. Most of those objects are things like BlockPos instances that are created, used once, and immediately discarded. That rate of object churn is what GC is fighting against constantly.

JIT Warmup. Java bytecode doesn't run directly as machine code. The JVM interprets it first, then a tiered JIT compiler watches which code paths are "hot" and compiles them to native machine code. The main JIT compiler, C2, produces highly optimized code but takes time to identify what to compile and time to actually compile it. A freshly started Minecraft server is operating below peak performance for its first 5-20 minutes as this warmup happens. The AOT caching work landing across Java 24-26 is specifically about cutting this warmup time.

CPU Throughput — The JIT's Code Quality. Once warm, how good is the native code the JIT generates? How efficiently is it using your CPU's capabilities? This is where things like auto-vectorization, loop optimizations, and the quality of compiled write barriers matter. Better JIT = more game simulation completed per CPU cycle = more headroom before your tick budget runs out.

Understand these three things and you can evaluate every JVM change in terms of how it moves the needle on a Minecraft server.

Java 21 is still a Fine Choice

Java 21 is the current Long-Term Support release. Mojang officially requires it for Minecraft 1.20.5 and later. It gets premier support until at least 2028. If you're running a stable 1.21.x server, Java 21 is the right call and nothing in this article should make you feel bad about that.

But Java 21 did bring genuinely significant things that established the foundation for everything since:

Generational ZGC (JEP 439) landed as a production-ready feature. This matters a lot. The Z Garbage Collector existed before Java 21, but it was non-generational — meaning it treated every single object in your heap equally when deciding what to collect. This is wildly inefficient for Minecraft because the vast majority of objects (all those BlockPos instances, packet objects, AI calculation results) die within milliseconds of being created. Most objects die young. A generational GC exploits this by collecting the "young generation" of the heap much more frequently and cheaply, only touching the "old generation" occasionally.

Non-generational ZGC had to scan your entire 8GB+ heap on every collection. Generational ZGC scans the small young generation constantly (cheap, fast) and touches the old generation rarely. The practical result for Minecraft servers: much lower and more consistent GC pause times. The Obydux research into Minecraft startup flags notes this as the most significant GC advancement for Minecraft in years, and large-server Minecraft communities have validated it. On a 100+ player server with 16GB+ heap allocated, generational ZGC can bring peak GC pause times from 200+ms (G1GC spikes) down to single-digit milliseconds consistently.

Virtual Threads (JEP 444, Project Loom) finalized. The main Minecraft tick loop is still single-threaded and virtual threads don't change that. But PaperMC and its derivatives use threads for async chunk loading, networking (Netty), and various background tasks. Virtual threads make those background operations more scalable. The Leaf MC documentation notes that the modern JVM (Java 21+) has gotten good enough at handling Minecraft that even default GC configuration produces reasonable results — which would have been an absurd statement on Java 8.

We can skip Java 22

Java 22 was a short-term release — six months of Premier support, released March 2024. It's end-of-life. If someone is running Java 22 on a production server right now they should upgrade immediately, either to 21 LTS if staying on current stable Minecraft, or to 25 LTS for forward compatibility.

That said, Java 22 shipped work that subsequent versions built on. The Foreign Function and Memory API (from Project Panama) finalized in Java 22. This is the "native access" work that Maddy Miller's Java/Minecraft analysis has highlighted as relevant to Netty (Minecraft's networking library) and LWJGL (the rendering library). Panama allows Java to interop with native CPU capabilities more efficiently, and Netty can theoretically use this for faster network I/O. The performance gains here are upstream library improvements — they happen when Netty and LWJGL ship updated versions, not just when you upgrade Java.

Java 22 also shipped G1GC write barrier improvements that were the early work of what became the major optimization in Java 24. Think of Java 22 as laying the groundwork.

Bottom line on Java 22: Don't run it. It's EOL. But understand it as a step in the sequence.

The C2 MergeStore Optimization — This One Is Specifically Relevant to Minecraft

Java 23 shipped something that I want to explain in detail because it's directly relevant to what a Minecraft server does all day: the C2 MergeStore optimization.

The Inside.java JDK 25 performance article describes this as allowing the C2 JIT compiler to merge byte-by-byte store operations into wider primitive stores. What that means in plain language: code that reads and writes byte arrays gets faster because the CPU can do multiple bytes of work in a single instruction instead of processing them individually.

You may be wondering why this is specifically relevant to a Minecraft server. Here's the thing: Minecraft's network stack runs on Netty, and every packet you send to every player gets serialized into a byte buffer. Every chunk of world data that gets sent when players move is byte arrays. Every NBT tag that describes block state, inventory contents, or entity data is processed as byte arrays. The packet encoding and decoding that happens every single tick, for every single connected player, goes through exactly this kind of byte-level operation. The C2 MergeStore optimization makes this work more efficient, and on a server under load, that kind of throughput improvement compounds.

Java 23 also fixed the secondary supertype cache scalability problem on x64 and AArch64 (JDK-8180450). This is a JVM internal data structure used for instanceof checks and virtual method dispatch. The old implementation used a single-element cache that caused catastrophic cache line ping-ponging on multi-threaded workloads — when multiple threads are rapidly checking different type hierarchies (which they are on any Paper server doing async chunk work), they'd constantly invalidate each other's cached values. The new hash-table based approach eliminates this contention. For Paper with its async chunk loading pipeline, this matters.

Java 23 is also the version where non-generational ZGC was deprecated. That's the signal: if you're using -XX:+UseZGC without -XX:+ZGenerational, your ZGC setup is now running on deprecated code and will stop working in Java 24+. In Java 24 and beyond, -XX:+UseZGC automatically means generational mode.

Java 23 in summary for servers: Real improvements from MergeStore and the supertype cache fix. Still a short-term release, still EOL now. Don't run it in production.

The Big G1GC JIT Win from Java 24

Java 24 is where things get interesting. It shipped JEP 475: Late Barrier Expansion for G1, and this is a meaningful performance improvement if you're running G1GC.

Here's what it means. G1GC uses "write barriers" — small pieces of code injected around every reference store operation (every time your code stores a reference to an object in a field). These write barriers let the GC track which objects in the old generation point to objects in the new generation. They're necessary for generational GC to work correctly.

The problem was that G1's write barriers were huge. Each write barrier required over 100 IR (intermediate representation) operations in C2's pipeline. A single write barrier compiled to roughly 50 x64 instructions. Since memory writes happen constantly in any Java program — and in Minecraft they happen on literally every entity update, every block change, every inventory modification — this overhead was significant.

The previous "early expansion" approach meant C2 was including all those write barrier IR nodes throughout its entire compilation pipeline, even though GC barriers don't benefit from most C2 optimizations. The JDK engineers found that G1 barrier IR operations were accounting for around 20% of C2's total execution time. Moving to "late expansion" — where the barrier instructions are inserted at the very end of compilation instead of being carried through the whole pipeline — eliminates that wasted work while producing equivalent output code.

The result: 10-20% less JIT compilation work for the combination of G1GC and C2. Every method the JIT compiles is cheaper to compile. The server warms up faster. More CPU is available for your actual game logic during warmup. And on an ongoing basis, recompilation of methods as the JIT's optimizer adapts has less overhead.

Java 24 also shipped the first AOT cache (JEP 483, Project Leyden). You can now run a "training" session of your server, save the class loading and linking work to a cache file, and subsequent startups skip all that work. Minecraft server startup is notoriously slow — often 60-120 seconds — largely because of class loading, linking, and initial JIT compilation. The AOT cache cuts into that.

Additionally, String concatenation got 40% faster at startup in Java 24 from a JIT rewrite. Server startup logs and config loading are heavy on string operations. Shaving startup time matters when you're running a server people are waiting to join.

Java 24 in summary: The G1GC JIT improvement is real and directly applicable to Minecraft. The AOT cache is useful for startup time. But it's another STR, end-of-life now. Don't run it.

Java 25 is the upgrade worthy TLS you want

Java 25 is the LTS release after Java 21. Premier support through at least 2028. This is the realistic upgrade target for any server admin who wants to get off Java 21.

Java 25 stacks every improvement from 22, 23, and 24 and adds more on top:

Auto-Vectorization Improvements. C2's auto-vectorization — the ability to rewrite plain Java loops to use SIMD (Single Instruction, Multiple Data) CPU instructions automatically — got meaningfully extended. The Inside.java performance article shows a Math.min/Math.max clipping benchmark getting 3-5x speedups on various platforms after auto-vectorization kicks in. The realistic impact on Minecraft code isn't 3-5x across the board — targeted benchmarks are best-case. But the kinds of loops that benefit most are exactly the math-heavy, array-processing code that shows up in pathfinding, chunk compression, physics simulation, and redstone computation. The JIT compiler doing more work for you, on code you didn't have to touch, is always free performance.

String::hashCode Constant Folding. HashMap lookups where the key is a string constant now get better JIT treatment — the JIT can recognize that "someConstantString".hashCode() is always the same value and avoid recomputing it. Minecraft plugins use HashMap-based registries constantly: command registration, permission node lookups, material registries, event handler maps. This is a small per-operation improvement that's pervasive enough to compound.

AOT Cache Extended with Method Profiles (JEP 515). Java 24's AOT cache saved class loading state. Java 25's extends it to also save the JIT's profiling information from the training run. This means on startup, instead of the JIT needing to observe which code paths are hot before it can optimize them, it already knows from the saved profiles. The Inside.java article notes some programs starting 15-25% faster with this versus Java 24 with a similarly trained cache. For a Minecraft server, the period between "server started" and "server performing well" gets shorter.

Compact Object Headers (-XX:+UseCompactObjectHeaders). This flag, stabilizing in Java 25, reduces the overhead per Java object header from 96-128 bits down to 64 bits. Every object in your JVM heap has a header. Minecraft creates millions of small objects — BlockPos, Vec3d, ChunkPos, packet objects, entity state snapshots. Reducing header size reduces heap pressure across all of them. The Obydux Minecraft flags analysis notes this can reduce memory usage by up to 20% in some cases. In Minecraft's workload the real number will be less dramatic, but any heap pressure reduction means the GC needs to run less often, which means fewer pauses.

Generational Shenandoah finalized. Shenandoah is a concurrent, low-pause GC available in Red Hat and Adoptium builds. Java 25 finalizes its generational mode. This doesn't replace generational ZGC as the recommendation for high-player-count servers, but it does mean Shenandoah is now a viable tested option for server admins who want a concurrent GC but hit issues with ZGC's memory overhead.

GraalVM CE JIT improvements. Java 25's GraalVM Community Edition JIT produces better code in more situations. If you're using GraalVM for your server (the brucethemoose benchmark repository has tested this extensively), Java 25 gets you the latest iteration.

Java 25 is the realistic "upgrade from 21" target. It's LTS. Every improvement from Java 22, 23, and 24 is included. For 1.21.x servers that want better performance without chasing the bleeding edge, Java 25 is the answer.

What Java 26 Specifically Adds

JDK 26 is a short-term release. Six months of premier support after the March 17, 2026 release date. Not an LTS. If you run it on a production server, you need a plan to upgrade to Java 27 (or wait for whatever the next LTS is, which will be Java 29) within six months. That's real operational overhead.

But let's talk about what it actually brings for servers, because this is the question:

G1GC Synchronization Overhead Reduction (JEP 522). Java 26 reduces the amount of synchronization required between application threads and GC threads specifically in G1. This is a direct throughput improvement for the most common Minecraft server configuration. The JEP text frames it clearly: G1 performs more of its work concurrently with the application compared to throughput-oriented collectors like Parallel GC, but this concurrency requires coordination between your application threads and the GC threads, which both lowers throughput and adds latency.

Java 26 reduces this synchronization overhead and shrinks the size of G1's injected write barrier code further. It builds directly on Java 24's late barrier expansion work. This means the write barriers that fire on every reference store in your game loop are slightly cheaper to execute. On a server doing hundreds of millions of reference stores per minute across entity updates, physics calculations, and redstone processing, "slightly cheaper" adds up to measurable sustained TPS improvement under load.

This is the key point about Java 26's G1 improvements: you don't change your startup flags, you don't change your GC configuration, you just upgrade and G1GC gets faster. That's the kind of improvement worth tracking.

AOT Object Caching — GC-Agnostic (JEP 516). Java 24 introduced the AOT cache. Java 25 extended it with method profiles. Java 26 makes it GC-agnostic. The previous implementation stored cached objects in GC-specific memory layouts, meaning if you built the AOT cache with G1, you couldn't use it with ZGC. Java 26 fixes this — cached objects are stored in a neutral format that any GC can load from. If you're running generational ZGC on Java 26 (which you should be if you have a large server), you can now also benefit from the full AOT cache. Previously these two optimizations were mutually exclusive. Java 26 removes that constraint.

Structured Concurrency — 6th Preview (JEP 525). Six preview cycles. It's not finalized but it's stable in practice. Folia — the regionized multithreading fork of Paper — and future Paper async work can use structured concurrency primitives to write cleaner, safer parallel code. This doesn't impact you as a server admin today, but it's the language infrastructure that server software developers are building on.

Applet API Removal (JEP 504). java.applet is gone. This matters exactly zero to you as a Minecraft server admin, but it reduces JDK footprint slightly. Good riddance.

What Java 26 does NOT do for Minecraft servers: It doesn't make the single-threaded tick loop multi-threaded. It doesn't eliminate GC pauses (though it reduces them). It doesn't make plugins faster. It doesn't fix whatever your timings report says is eating 15ms per tick in your shop plugin.

The Garbage Collector Decision

Here's the uncomfortable truth: your GC choice has more impact on Minecraft server performance than the difference between Java 21 and Java 26. Both decisions matter, but GC choice comes first.

G1GC with Aikar's Flags — Still The Right Default

For most servers — under 100 players, 4-16GB heap allocated — Aikar's G1GC flags are still the recommendation. They've been stress-tested in production for years on servers of every size and plugin stack. Use them:

java -Xms10G -Xmx10G \
  -XX:+UseG1GC \
  -XX:+ParallelRefProcEnabled \
  -XX:MaxGCPauseMillis=200 \
  -XX:+UnlockExperimentalVMOptions \
  -XX:+DisableExplicitGC \
  -XX:+AlwaysPreTouch \
  -XX:G1NewSizePercent=30 \
  -XX:G1MaxNewSizePercent=40 \
  -XX:G1HeapRegionSize=8M \
  -XX:G1ReservePercent=20 \
  -XX:G1HeapWastePercent=5 \
  -XX:G1MixedGCCountTarget=4 \
  -XX:InitiatingHeapOccupancyPercent=15 \
  -XX:G1MixedGCLiveThresholdPercent=90 \
  -XX:G1RSetUpdatingPauseTimePercent=5 \
  -XX:SurvivorRatio=32 \
  -XX:+PerfDisableSharedMem \
  -XX:MaxTenuringThreshold=1 \
  -Dusing.aikars.flags=https://mcflags.emc.gs \
  -Daikars.new.flags=true \
  -jar server.jar nogui

If you're on Java 25+, add these — they're supported from Java 25 and Mojang considers them beneficial enough to include in their own default flags for future Minecraft versions:

  -XX:+UseCompactObjectHeaders \
  -XX:+UseStringDeduplication \

The reason Aikar's flags tune G1 so heavily toward young generation is exactly what I explained earlier: Minecraft generates an extraordinary amount of short-lived objects. By giving G1 more young generation space (30-40% of heap), those objects die in cheap young GC collections instead of getting promoted to old generation where they're expensive to collect. The MaxTenuringThreshold=1 flag means objects that survive even one young GC get promoted immediately to old gen — this sounds counterintuitive but reduces the cost of survivor space processing for Minecraft's specific allocation patterns.

If you have more than 12GB allocated: Adjust to G1NewSizePercent=40, G1MaxNewSizePercent=50, G1HeapRegionSize=16M, G1ReservePercent=15, InitiatingHeapOccupancyPercent=20. Larger regions handle Minecraft's "humongous allocations" (objects larger than half the region size that go directly to old gen) more efficiently.

Generational ZGC — The High-Player-Count Answer

For servers running 100+ players with 16GB+ heap available, switch to Generational ZGC. The goal with ZGC is near-zero stop-the-world pause time. ZGC does almost all its work concurrently with your application. The tradeoff is it needs more RAM and more CPU cores to run those concurrent GC threads.

java -Xms16G -Xmx16G \
  -XX:+UseZGC \
  -XX:+ZGenerational \
  -XX:+AlwaysPreTouch \
  -XX:+DisableExplicitGC \
  -XX:+ParallelRefProcEnabled \
  -XX:+PerfDisableSharedMem \
  -XX:+UseCompactObjectHeaders \
  -XX:+UseStringDeduplication \
  -jar server.jar nogui

On Java 23+, -XX:+ZGenerational is implied when you use -XX:+UseZGC. On Java 25+, it's the only ZGC mode. Include the flag anyway for clarity in your startup script — future you will thank current you.

ZGC Requirements that disqualify many setups

  • You need more RAM than G1GC — provision 20-25% more heap than you would with G1. If G1 works great at 8GB, ZGC needs 10-12GB to run well.
  • You need more CPU cores — ZGC's concurrent threads need headroom. On a 2-core VPS, ZGC competes with your game simulation for CPU time and hurts you.
  • ZGC is not available in Oracle's official JDK on all platforms. Use Adoptium Temurin (wide availability, community-supported), Amazon Corretto (production-ready), or the Microsoft Build of OpenJDK (Mojang's preferred distribution) to guarantee you have ZGC available.

The real-world case for ZGC

The 1,000-player Folia stress test documented by Cubxity used generational Shenandoah GC on Java 21 with a 100GB heap. At 600 players they measured 7.9GB/s heap allocation rate. At that scale, a GC that can do concurrent collection without stopping the world isn't nice to have — it's the difference between 20 TPS and an unplayable server.

What About Shenandoah?

Shenandoah is another concurrent low-pause GC, available in Adoptium, Corretto, and Red Hat builds (not Oracle's JDK). The brucethemoose benchmark research recommends Shenandoah for Minecraft clients but ZGC for powerful Java 17+ servers. The concern with Shenandoah on servers is that its concurrency model can compete more aggressively with your game simulation threads for CPU time than ZGC does. Generational Shenandoah (finalized in Java 25) is better than the non-generational version, but ZGC is generally better tested at high Minecraft server player counts.

Try Shenandoah if you want to experiment. Use ZGC if you want the community-validated answer.

The Server Software Matters Too

I want to be honest with you about something. The performance difference between Java 21 and Java 26 is real but incremental. The performance difference between server software choices is massive.

  • Vanilla → Paper: This is the biggest single performance improvement available to any Minecraft server admin, with zero Java version change. Paper implements dozens of micro-optimizations across entity AI, chunk loading, redstone, and mob spawning. It makes chunk loading asynchronous in ways vanilla doesn't. It fixes the algorithmic complexity of various vanilla systems. Paper on Java 21 beats vanilla Minecraft on Java 26 every time. It's a drop-in replacement for Spigot — if you're running vanilla or Spigot, just switch.
  • Paper → Purpur: Purpur is a Paper fork with additional configuration options and some extra patches. Not a dramatic performance difference but more control over things like entity behavior that can be tuned for your specific workload.
  • Paper → Folia (for specific use cases): Folia is Paper's experimental regionized multithreading fork, built by the same team. Instead of a single main thread ticking the entire world, Folia divides the loaded world into independent regions that tick in parallel on a thread pool. For servers where players are spread out — SkyBlock, large SMP maps, anarchy servers — this can dramatically increase effective player capacity because multiple regions are ticking simultaneously. The PaperMC documentation recommends at least 16 cores for Folia to be beneficial, and it breaks most plugins. It's not a drop-in replacement. But the ceiling it removes is real — the Cubxity 1,000-player stress test ran on Folia with generational Shenandoah on Java 21 and held 600 players at 20 TPS on consumer hardware.

The Java version upgrade gets you incremental JVM improvements. Switching server software gets you architectural improvements. Do both, but understand the order of impact.

What Profiling Actually Looks Like

Every optimization conversation about Minecraft servers eventually comes down to this question: what is actually consuming your tick budget? You cannot optimize what you cannot measure, and guessing based on vibes is how you spend three hours tuning JVM flags that weren't your bottleneck.

Install Spark. It's a performance profiler built specifically for Minecraft servers. It integrates with Paper, Purpur, Folia, and others. Run these commands when your server is under load:

/spark profiler  # Identifies where tick time is being spent
/spark gcmonitor  # Shows GC pause frequency and duration in real time
/spark tps  # Shows current TPS and tick duration

The gcmonitor output will tell you immediately whether your GC is the problem. If you're seeing old generation collection pauses over 50ms, your GC configuration needs tuning before anything else. If you're seeing young generation pauses of 20-30ms every 2-3 seconds, you might need more heap or a different GC. If you're seeing no GC issues but low TPS, the profiler will tell you which plugins or game systems are eating your tick time.

Enable GC logging in your startup flags to get a permanent record:

-Xlog:gc*:logs/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

This creates rotating log files in your server's logs/ directory. Any Pause Full events in those logs are stop-the-world pauses — find them, understand what triggered them, and eliminate them.

The Actual Decision Framework

Alright. You've read the research. Here's what to actually do.

You're running 1.21.x on a stable server under 100 players

Stay on Java 21 LTS unless something is broken. If you're having GC pause problems, fix your GC flags first. The gains from upgrading to 25 or 26 without also addressing GC configuration are modest.

You're running 1.21.x and want to future-proof

Upgrade to Java 25 LTS. Not Java 22, 23, 24, or 26 — Java 25. It includes everything from the intermediate versions. It's LTS. Your plugins built for Java 21+ will run on it. Install Adoptium Temurin 25 or the Microsoft Build of OpenJDK 25.

You're running 100+ players and fighting GC pauses:

Switch to Generational ZGC on Java 21 or 25 before worrying about Java version. This is the highest-impact change available. Measure with spark gcmonitor before and after.

You're planning for the future:

Java 26 is interesting for G1GC throughput and the GC-agnostic AOT cache. But it's a short-term release — six months of support. If you want the cutting edge without the operational overhead of tracking STR releases, wait: Java 29 will be the next LTS after Java 25, expected around September 2027. Java 26 right now is for people who want to test and benchmark, not for production deployments with no migration plan.

You're running Forge or Fabric mods

Test everything before deploying. Mods interact with JVM internals more aggressively than plugins do. Java 25 is generally fine for modern mod loaders. Java 26 is less tested with the mod ecosystem as of today. Run a staging server with your full mod list before touching production.

You want to skip Java 22 and 23 specifically:

Yes, skip them. They're both end-of-life. All their improvements are included in Java 24, 25, and 26. There's no reason to run them in production today.

The Performance Impact Summary

Here's the honest version of what you actually get, in terms relevant to a Minecraft server:

Change Java 21 Java 22-23 Java 24 Java 25 Java 26
Generational ZGC ✅ Final ✅ Non-gen removed ✅ Only mode ✅ + AOT compatible
G1GC Write Barriers Baseline Early improvements Major improvement (-20% JIT work) Extended Further reduced
AOT Cache ✅ Classes only ✅ Classes + profiles ✅ GC-agnostic
C2 MergeStores (byte ops) ✅ Extended
Secondary Supertype Cache ✅ x64/AArch64 ✅ All platforms
Auto-vectorization Baseline Minor Minor Major expansion
Compact Object Headers Preview ✅ Stable
String hashCode folding

Realistic TPS improvement from Java version upgrade alone (same server, same plugins, same GC):

  • Java 21 → Java 25: Roughly 5-15% better sustained throughput on a loaded server. More noticeable on servers already running at high tick load (17-19 TPS) where any efficiency gain manifests as headroom.
  • Java 21 → Java 26: Marginally better than 25. The G1GC synchronization reduction in 26 is real but not dramatic over 25.
  • Java 21 → Java 25, also switching G1GC to Generational ZGC with proper heap sizing on a 100+ player server: Much more significant — GC pause spikes that were making players rubber-band disappear. TPS stability improves substantially.

The honest truth is that no Java version upgrade is going to take a server that's drowning at 10 TPS to stable 20 TPS. If you're that far under the load ceiling, you have plugin problems or infrastructure problems. Java version is the last 5-15% of optimization, not the first.

The Cncusion

The question was: real-world performance gains from upgrading Java 21 → 22 → 23 → 26?

  • Java 22 and 23 specifically: Short-term releases, already EOL. Real technical improvements landed in both, but you should not be running either in production in 2026. Skip them.
  • Java 24: G1GC JIT improvement is the most impactful Minecraft-relevant performance change in years. But it's also EOL. Included in 25.
  • Java 25: The right upgrade target. LTS. Cumulative improvements from 22-24 plus its own additions. Generational ZGC is rock solid. AOT cache with method profiles speeds up server startup. Compact object headers reduce heap pressure. This is a genuinely better runtime than Java 21 for Minecraft.
  • Java 26: The G1GC throughput improvement and GC-agnostic AOT cache are meaningful additions. But it's a short-term release with six months of support. Run it in staging, test it, benchmark it. Don't put it on production unless you're willing to track the upgrade cycle.
  • GC configuration beats Java version. If you're on Java 21 with poorly tuned G1GC flags, switching to Java 26 with the same broken flags doesn't help much. Fix the flags first. Or switch to generational ZGC if your player count warrants it.
  • Server software beats everything. If you're on vanilla, switch to Paper. The Leaf documentation, the brucethemoose benchmarks, and Aikar's years of research all point at the same conclusion: the JVM optimizes running Paper better than running vanilla, your plugins are more isolated from the game loop, and async chunk loading alone is a larger throughput improvement than any Java version bump.
  • Measure everything. Install Spark. Run /spark gcmonitor under real player load. Know what your actual GC pause pattern looks like before you change anything. Change one thing at a time. Measure again. The server admin community has been doing cargo-cult flag copying for years — don't be part of that.

The Java ecosystem is genuinely moving faster than it has in a decade. JDK 26's feature list reflects a JVM team that is shipping real engineering work in every release cycle. The cumulative impact from Java 21 to Java 25 is real and worth capturing — just do it on the LTS, not the STR.