Java 25 New Features — What Changed and Why Minecraft Upgraded
First — What Is an LTS Release and Why Does It Matter?
Java ships a new version every 6 months. Most of these are short-term releases — supported for 6 months and then dropped. LTS (Long-Term Support) releases are different — they get security patches and bug fixes for years.
Java 25 is LTS. That means: - Oracle and major vendors support it until at least 2030 - Cloud providers like AWS and Google Cloud optimise their runtimes for it - Frameworks like Spring Boot 3.3+ and Quarkus fully certify against it - Large organisations actually upgrade to it
This is why Minecraft waited. They weren't going to require Java 22 and then have to require Java 25 a year later. They skipped straight to the LTS.
For your own projects, the practical takeaway is simple: if you're on Java 21, start planning an upgrade. Java 17's LTS support ends in 2026.
ZGC Is Now the Default GC — This One Actually Matters
This is the change most developers will feel without changing a single line of code. Up until Java 25, the default garbage collector was G1GC. G1 is good — but it has stop-the-world pauses. For most apps this is fine. For latency-sensitive apps (game servers, trading systems, real-time APIs), those pauses are a problem.
ZGC keeps pauses under 1 millisecond regardless of heap size. It does this by doing most of its work concurrently — while your application is still running. Minecraft's chunk loading used to cause noticeable lag spikes because G1GC would kick in during heavy world generation. ZGC smooths that out.
The best part: you don't have to do anything. No JVM flags, no config changes. If you were already using -XX:+UseZGC, you can remove that flag.
package io.thecodeforge.monitoring; import java.lang.management.ManagementFactory; import java.lang.management.GarbageCollectorMXBean; import java.util.List; /** * A simple utility to verify the active Garbage Collector. * No more guessing if ZGC is actually doing the heavy lifting. */ public class GCChecker { public static void main(String[] args) { List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); System.out.println("--- TheCodeForge Runtime Monitor ---"); System.out.println("Active Garbage Collectors for this JVM instance:"); for (GarbageCollectorMXBean bean : gcBeans) { System.out.printf(" - %s (Collections: %d, Total Time: %dms)%n", bean.getName(), bean.getCollectionCount(), bean.getCollectionTime()); } } }
Active Garbage Collectors for this JVM instance:
- ZGC Cycles (Count: 12, Time: 0ms)
- ZGC Pauses (Count: 24, Time: 8ms)
Compact Object Headers — Less RAM, Same Code
Every Java object carries a header — JVM bookkeeping data. In Java 21 and earlier, this is typically 96 to 128 bits. Java 25 ships compact object headers (JEP 450) as a stable feature, cutting that down to 64 bits.
Now 32 bits doesn't sound like a lot. But a typical backend application might have tens of millions of live objects. Across that many objects, you're looking at a meaningful drop in heap usage — less GC pressure, better cache locality, and lower cloud bills.
# Production Dockerfile for Java 25 applications # We use the Temurin distribution for stable LTS support FROM eclipse-temurin:25-jdk-jammy WORKDIR /app COPY target/forge-service.jar app.jar # Pro-tip: Java 25 automatically optimizes object headers on 64-bit systems. # You can verify this in logs by adding -Xlog:cds=debug during startup. ENTRYPOINT ["java", "-Xmx2g", "-jar", "app.jar"]
Pattern Matching for switch — Finally Out of Preview
This one has been in preview since Java 17. Four release cycles later, it's fully finalised in Java 25 — which means it's stable, won't change, and you can use it without --enable-preview.
If you've written a lot of instanceof chains, you'll like this.
package io.thecodeforge.logic; public class ShapeProcessor { // Sealed hierarchy ensures the switch is exhaustive at compile-time public sealed interface Shape permits Circle, Rectangle, Triangle {} public record Circle(double radius) implements Shape {} public record Rectangle(double width, double height) implements Shape {} public record Triangle(double base, double height) implements Shape {} public static double calculateArea(Shape shape) { return switch (shape) { case Circle c -> Math.PI * Math.pow(c.radius(), 2); case Rectangle r -> r.width() * r.height(); case Triangle t -> 0.5 * t.base() * t.height(); // Default is unnecessary and actually discouraged with sealed types! }; } public static String categorizeInput(Object obj) { return switch (obj) { case Integer i when i > 100 -> "High-capacity Integer"; case Integer i -> "Standard Integer"; case String s when s.isBlank() -> "Empty input string"; case String s -> "Valid string: " + s; case null -> "Null reference detected"; default -> "Unsupported type"; }; } }
Record Patterns — Destructure in One Line
Also finalised in Java 25. Record patterns let you unpack record components directly inside a pattern match — no separate accessor calls needed.
package io.thecodeforge.models; public class PatternDemo { record Point(int x, int y) {} record Window(Point topLeft, Point bottomRight) {} public static void printDiagnostics(Object obj) { // Nesting record patterns for deep destructuring if (obj instanceof Window(Point(int x1, int y1), Point(int x2, int y2))) { int width = Math.abs(x2 - x1); int height = Math.abs(y2 - y1); System.out.printf("Rendering Window [%dx%d] starting at (%d,%d)%n", width, height, x1, y1); } } }
Virtual Threads Are Stable — Use Them
Virtual threads were finalised in Java 21, but Java 25 fixes a bunch of edge cases that made people hesitant to use them in production — particularly around thread-local variables and synchronisation with native code.
If you're running a web server and still using a fixed thread pool, switch to virtual threads. The code change is one line.
package io.thecodeforge.web; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrencyConfig { /** * Returns an executor that spawns a new virtual thread for every task. * Ideal for blocking I/O operations like database queries or REST calls. */ public static ExecutorService getVirtualExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); } public void handleAsyncRequest(Runnable task) { // Lightweight thread creation Thread.ofVirtual() .name("forge-worker-", 1) .start(task); } }
So Why Did Minecraft Really Upgrade?
Three reasons, honestly:
ZGC by default — chunk loading in large worlds triggered G1GC pauses that players felt as lag. ZGC's sub-millisecond pauses remove that.
Compact object headers — Minecraft tracks millions of blocks, entities, and chunks simultaneously. Cutting header size reduces baseline memory pressure, which is why Mojang could confidently bump the default launcher RAM to 4GB without it feeling wasteful.
It's LTS — Mojang isn't going to build on a release that's out of support in 6 months. Java 25 takes them through to 2030. Same reason they were on Java 21 before this.
There's also a fourth, less talked-about reason: Java 25 is the first version to fully unobfuscate the Minecraft codebase under the new release process. But that's more of an internal tooling story than a Java feature story.
How to Actually Upgrade
The migration from Java 21 to Java 25 is low-friction for most projects. There are no major breaking API changes.
# 1. Install Java 25 (Temurin) via SDKMAN sdk install java 25-tem sdk default java 25-tem # 2. Update your Maven pom.xml # <properties> # <java.version>25</java.version> # <maven.compiler.release>25</maven.compiler.release> # </properties> # 3. Or update your Gradle build.gradle # java { # toolchain { # languageVersion = JavaLanguageVersion.of(25) # } # } # 4. Clean and verify ./mvnw clean verify
🎯 Key Takeaways
- Java 25 is LTS — supported until 2030, the version enterprises and Minecraft standardise on
- ZGC is now default — sub-millisecond GC pauses with zero config changes needed
- Compact object headers cut per-object memory overhead roughly in half
- Pattern matching for switch and record patterns are fully finalised — no more --enable-preview
- Virtual thread edge cases are fixed in Java 25 — safe to use in production now
- Migration from Java 21 is straightforward — no major breaking changes
⚠ Common Mistakes to Avoid
- ✕Mistake: Manually enabling ZGC with -XX:+UseZGC. Fix: In Java 25, it is default; the flag is redundant but harmless. However, check if your specific cloud vendor overrides this.
- ✕Mistake: Assuming Compact Object Headers work on 32-bit JVMs. Fix: JEP 450 is specifically targeted at 64-bit platforms.
- ✕Mistake: Using virtual threads for CPU-bound tasks (like crypto or video encoding). Fix: Virtual threads are for IO-bound tasks; use platform threads or parallel streams for heavy calculation.
Interview Questions on This Topic
- QExplain the difference between ZGC and G1GC. Why would a low-latency application prefer ZGC in Java 25?
- QWhat are Compact Object Headers (JEP 450), and how do they contribute to reduced heap usage?
- QHow does sealed interface support improve pattern matching in switch expressions?
- QWhat is the difference between a Platform Thread and a Virtual Thread in Java 25?
Frequently Asked Questions
How do I check if my application is using ZGC or G1GC?
You can run your application with the flag -Xlog:gc::time. The very first lines of the output will explicitly state: 'Using The Z Garbage Collector' or 'Using G1'. Alternatively, use the ManagementFactory.getGarbageCollectorMXBeans() API to check programmatically.
Will my Java 21 code run on Java 25 without changes?
Almost certainly yes. Java 25 is backward compatible with Java 21 compiled code. The main exceptions are if you were relying on internal JDK APIs (sun.* packages) that were removed — but if you were doing that, you already knew it was risky.
Is ZGC actually better than G1GC for everything?
For latency, yes. ZGC's pause times are under 1ms regardless of heap size. G1GC can pause for tens or hundreds of milliseconds on large heaps. The tradeoff is that ZGC uses slightly more CPU for concurrent GC work. For throughput-focused batch jobs, G1GC can still be faster. But for servers and interactive applications, ZGC wins.
Why did Minecraft skip Java 22, 23, and 24?
Those are non-LTS releases — each supported for only 6 months. Mojang needs a stable base for years, not months. Java 25 gives them that.
Does Spring Boot 3 support Java 25?
Spring Boot 3.3+ officially supports Java 25. If you're on an older Spring Boot 2.x version, you will likely need to upgrade to the 3.x branch first to ensure compatibility with Jakarta EE and the newer bytecode version.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.