Java

There is no AI here. All content is human-authored and tested for accuracy.

Compare Java 21 to Java 25

Compare Java 21 vs Java 25 for Mac. Differences in performance, features, and support timelines. Recommended version by use case.

Java 21 and Java 25 are both long-term support (LTS) releases, which makes choosing between them a significant decision. Java 21 arrived in September 2023 with virtual threads and pattern matching. Java 25 followed in September 2025 with 22% memory savings, faster startup, and fixes for virtual thread limitations. This guide helps you understand the differences, install both versions, and decide which one fits your projects.

The two-year LTS cadence means you get a new LTS version every two years instead of three. Java 21 is the proven baseline with mature tooling. Java 25 is the latest LTS with substantial performance improvements and a longer support runway extending to 2033. New projects should target Java 25. Existing Java 21 codebases can migrate at a measured pace.

Before you get started

You'll need a terminal application to work with Java. Apple includes the Mac terminal but I prefer Warp Terminal. Warp is an easy-to-use terminal application, with AI assistance to help you learn and remember terminal commands. Download Warp Terminal now; it's FREE and makes coding easier when working with Java.

Before you begin

Check whether you already have Java installed:

$ java -version

If Java is installed, you'll see version information. If you see "java: command not found," continue with this guide.

You'll also need Homebrew. If you don't have it installed, see our Homebrew installation guide. Homebrew makes installing and updating Java straightforward.

Check your Mac's processor type:

$ uname -m

The output tells you which architecture you have: "arm64" means Apple Silicon (M1, M2, M3, or M4), while "x86_64" means Intel. Homebrew handles architecture detection automatically.

Understanding the LTS versions

Oracle shifted from a three-year to a two-year LTS cycle starting with Java 21. This change addressed developer frustration that enterprise deployments couldn't adopt new features fast enough.

Release timeline

Here's what happened between Java 21 and Java 25:

  • Java 21 (September 19, 2023) — LTS release with 15 JEPs, virtual threads finalized
  • Java 22 (March 19, 2024) — Feature release with 12 JEPs, FFM API finalized
  • Java 23 (September 17, 2024) — Feature release with 12 JEPs, string templates withdrawn
  • Java 24 (March 18, 2025) — Feature release with 24 JEPs, virtual thread pinning fixed
  • Java 25 (September 16, 2025) — LTS release with 18 JEPs, compact headers and generational Shenandoah

Between Java 21 and 25, approximately 66 JEPs (JDK Enhancement Proposals) delivered features across four releases. Java 24 set a record with 24 JEPs, introducing critical fixes including JEP 491 for virtual thread pinning.

Support timeline comparison

Support duration matters for production systems. Here's how the two versions compare:

Java 21 support:

  • Free until September 2026 (Oracle NFTC)
  • Premier Support until September 2028
  • Extended Support until September 2031

Java 25 support:

  • Free until September 2028 (Oracle NFTC)
  • Premier Support until September 2030
  • Extended Support until September 2033

Java 25 gives you two additional years of free updates and extends support until 2033. For new projects, that longer runway often tips the decision.

Oracle licensing costs

Oracle's No-Fee Terms and Conditions (NFTC) license allows free commercial use until one year after the next LTS release. After the free period ends, Oracle charges per employee across your entire organization—not just developers:

  • 1-999 employees — $15/employee/month
  • 1,000-9,999 employees — $12/employee/month
  • 10,000+ employees — $8.25/employee/month

Most developers avoid this by using free distributions like Eclipse Temurin or Amazon Corretto.

Alternative distributions

Several vendors provide free OpenJDK builds with longer support timelines than Oracle:

  • Eclipse Temurin — Java 21 free until December 2029, Java 25 free until ~2031-2032. Vendor-neutral and TCK certified.
  • Amazon Corretto — Java 21 free until October 2030, Java 25 free until October 2032. Seven years free, AWS-backed.
  • Azul Zulu Community — Java 21 free until September 2031, Java 25 free until ~September 2033. Eight years free.

Recommendation: Use Eclipse Temurin for local development. It's TCK-certified (meaning it passes Java's compatibility tests), completely free, and installs cleanly via Homebrew. Amazon Corretto is an excellent choice if you deploy to AWS.

Install Java 21 and Java 25

The easiest approach is installing both versions side by side. This lets you switch between them depending on project requirements.

Install with Homebrew

Homebrew's Temurin casks automatically register with macOS, so /usr/libexec/java_home discovers them without extra configuration.

Install Java 21:

$ brew install --cask temurin@21

Install Java 25:

$ brew install --cask temurin@25

The --cask flag tells Homebrew to install a macOS application. This installs Java to /Library/Java/JavaVirtualMachines/ which is the standard location macOS expects.

Verify both installations:

$ /usr/libexec/java_home -V

You should see output listing both versions:

Matching Java Virtual Machines (2):
    25.0.1 (arm64) "Eclipse Adoptium" - "OpenJDK 25.0.1" /Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home
    21.0.6 (arm64) "Eclipse Adoptium" - "OpenJDK 21.0.6" /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home

Check the active version:

$ java -version

Read next: Install JDK on Mac for a complete comparison of Java distributions.

Set JAVA_HOME

Many Java tools—Maven, Gradle, IntelliJ, and others—use the JAVA_HOME environment variable to find your JDK. Add these lines to your ~/.zprofile file:

export JAVA_HOME=$(/usr/libexec/java_home -v 25)
export PATH=$JAVA_HOME/bin:$PATH

Apply the changes:

$ source ~/.zprofile

Verify it worked:

$ echo $JAVA_HOME
$ java -version

Read next: Set JAVA_HOME on Mac for complete shell configuration instructions.

Quick version switching

Add this function to ~/.zprofile for easy switching between versions:

jdk() {
    export JAVA_HOME=$(/usr/libexec/java_home -v"$1")
    java -version
}

Close and reopen your terminal, then switch versions instantly:

$ jdk 21
$ jdk 25

Alternative: SDKMAN

SDKMAN downloads and manages JDKs itself. Some developers prefer it for broader SDK support (Gradle, Maven, Kotlin):

$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"

List available Java versions:

$ sdk list java

Install specific versions:

$ sdk install java 21.0.6-tem
$ sdk install java 25.0.1-tem

Set a default:

$ sdk default java 25.0.1-tem

Switch for the current session:

$ sdk use java 21.0.6-tem

Alternative: jenv

If you install JDKs via Homebrew and want lighter-weight version management, jenv is a good choice:

$ brew install jenv

Add to ~/.zprofile:

export PATH="$HOME/.jenv/bin:$PATH"
eval "$(jenv init -)"

Register your installed JDKs:

$ jenv add /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
$ jenv add /Library/Java/JavaVirtualMachines/temurin-25.jdk/Contents/Home

Set versions globally or per-directory:

$ jenv global 25
$ jenv local 21

Recommendation: For most developers, the simple jdk() shell function is sufficient. Use SDKMAN if you manage multiple SDKs beyond Java. Use jenv if you need per-directory version control.

Read next: Java Version Managers for detailed comparisons.

Java 21 features (baseline)

Java 21 finalized several transformative features. Understanding these helps you appreciate what's already available before considering Java 25's additions.

Virtual threads

Virtual threads (JEP 444) enable millions of lightweight threads on a handful of OS threads. They're perfect for I/O-bound applications:

// Create a virtual thread
Thread.startVirtualThread(() -> {
    fetchDataFromDatabase();
});

// Or use an executor
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}

Virtual threads unmount from carrier threads when blocking on I/O, freeing those carriers for other work. This enables thread-per-request architectures without resource exhaustion.

Limitation in Java 21: Virtual threads pin to carrier threads inside synchronized blocks. This hurts scalability when mixing virtual threads with legacy synchronized code. Java 25 fixes this.

Pattern matching for switch

Type patterns and record deconstruction work directly in switch expressions:

String result = switch (obj) {
    case Integer i when i > 0 -> "Positive: " + i;
    case Integer i -> "Non-positive: " + i;
    case String s -> "String: " + s;
    case null -> "Null value";
    default -> "Other";
};

Record patterns

Destructure records directly in pattern matching:

record Point(int x, int y) {}

if (obj instanceof Point(int x, int y)) {
    System.out.println("x: " + x + ", y: " + y);
}

Sequenced collections

Three new interfaces provide encounter-order guarantees:

  • SequencedCollection — Methods like addFirst(), addLast(), getFirst(), getLast(), reversed()
  • SequencedSet — Ordered set with sequenced operations
  • SequencedMap — Ordered map with putFirst(), putLast()

These interfaces are now implemented by ArrayList, LinkedHashSet, LinkedHashMap, TreeSet, and TreeMap.

Preview features in Java 21

Several features were preview in Java 21 and have since evolved:

  • String templates (JEP 430) — Withdrawn in Java 23 due to design issues
  • Structured concurrency (JEP 453) — Still preview in Java 25
  • Scoped values (JEP 446) — Finalized in Java 25
  • Unnamed patterns and variables (JEP 443) — Finalized in Java 22

New features in Java 25

Java 25 brings significant improvements in performance, developer ergonomics, and virtual thread reliability.

Compact object headers

JEP 519 reduces object headers from 96 bits (12 bytes) to 64 bits (8 bytes) on 64-bit platforms. This Project Lilliput feature delivers substantial memory savings:

  • SPECjbb2015 heap space — 22% reduction
  • SPECjbb2015 CPU time — 8% reduction
  • G1/Parallel GC collections — 15% fewer
  • Typical live data — 10-20% reduction

Enable compact object headers:

$ java -XX:+UseCompactObjectHeaders -jar app.jar

This feature benefits applications with many small objects, memory-constrained environments, and container deployments where memory efficiency affects cost.

Flexible constructor bodies

JEP 513 lets you execute code before calling super() or this(). This solves a decades-old limitation:

// Java 21: Validation happens AFTER super() runs
class Employee extends Person {
    Employee(int age) {
        super(age);  // Must be first line
        if (age < 18 || age > 67) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// Java 25: Validation BEFORE super()
class Employee extends Person {
    Employee(int age) {
        if (age < 18 || age > 67) {
            throw new IllegalArgumentException("Invalid age");
        }
        super(age);  // Only runs if validation passes
    }
}

The code before super() (called the "prologue") has restrictions: you cannot use this, read instance fields, call instance methods, or access super fields or methods.

Compact source files and instance main methods

JEP 512 simplifies entry-level Java programming:

// Traditional Java (21 and earlier)
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

// Java 25 Compact Source
void main() {
    IO.println("Hello, World!");
}

Try it yourself:

$ echo 'void main() { IO.println("Hello from Java 25"); }' > Hello.java
$ java Hello.java
Hello from Java 25

Key concepts:

  • Implicit class — The compiler generates an unnamed class wrapping your code
  • Instance main method — Non-static entry point, no String[] args required
  • java.lang.IO — New console I/O class with println() and readln() methods

Module import declarations

JEP 511 lets you import entire modules with a single statement:

import module java.base;  // Imports ~54 packages

public class DataProcessor {
    public Map<String, List<Integer>> process(Stream<String> input) {
        return input.collect(Collectors.groupingBy(
            s -> s.substring(0, 1),
            Collectors.mapping(String::length, Collectors.toList())
        ));
    }
}

This eliminates import statement clutter for common packages like java.util.*, java.io.*, and java.time.*.

Scoped values

JEP 506 provides an alternative to ThreadLocal with better semantics for virtual threads:

// ThreadLocal: mutable, unbounded lifetime, expensive inheritance
private static final ThreadLocal<String> USER = new ThreadLocal<>();

// ScopedValue (Java 25): immutable, scoped, efficient
private static final ScopedValue<String> USER = ScopedValue.newInstance();

public void handleRequest(String userId) {
    ScopedValue.where(USER, userId).run(() -> {
        processRequest();  // USER.get() returns userId within this scope
    });  // Automatically unbound when scope ends
}

How scoped values compare to ThreadLocal:

  • Mutability — ThreadLocal is mutable (set/get), ScopedValue is immutable
  • Lifetime — ThreadLocal is unbounded, ScopedValue is scoped to block
  • Inheritance — ThreadLocal has expensive copying, ScopedValue has efficient sharing
  • Memory — ThreadLocal has higher footprint, ScopedValue has lower footprint

Stream gatherers

JEP 485 adds custom intermediate operations for streams:

import java.util.stream.Gatherers;

// Fixed-size batches (non-overlapping)
List<List<Integer>> batches = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
    .gather(Gatherers.windowFixed(3))
    .toList();  // [[1,2,3], [4,5,6], [7,8]]

// Sliding windows (overlapping)
List<List<Integer>> windows = Stream.of(1, 2, 3, 4, 5)
    .gather(Gatherers.windowSliding(3))
    .toList();  // [[1,2,3], [2,3,4], [3,4,5]]

// Rate-limited concurrent mapping
List<String> results = urls.stream()
    .gather(Gatherers.mapConcurrent(4, url -> fetchContent(url)))
    .toList();  // Max 4 concurrent fetches

Built-in gatherers include fold, mapConcurrent, scan, windowFixed, and windowSliding. You can compose them with andThen().

AOT method profiling

Project Leyden's AOT features (JEP 514, JEP 515) store preloaded classes and method profiles from training runs:

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconfig -jar app.jar
$ java -XX:AOTCacheOutput=app.aot -jar app.jar
$ java -XX:AOTCache=app.aot -jar app.jar

Startup improvements:

  • Hello World — 12% faster
  • Helidon Quickstart — ~4x faster
  • Spring Boot + Leyden — 2-4x faster

This benefits applications that load many classes at startup—common in frameworks like Spring Boot.

Generational Shenandoah

JEP 521 adds young/old generation support to Shenandoah GC:

$ java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -jar app.jar

Generational Shenandoah offers sub-10ms pauses with better throughput than non-generational mode. It's excellent for cloud microservices where consistent latency matters.

Virtual thread pinning fix

The virtual thread pinning fix deserves special attention because it removes a major limitation from Java 21.

The problem in Java 21

When a virtual thread held a monitor (entered a synchronized block) and then blocked on I/O, it "pinned" to its carrier thread. The carrier couldn't run other virtual threads until the blocking operation completed and the monitor was released. This hurt scalability when mixing virtual threads with legacy code using synchronized.

The workaround was migrating synchronized blocks to ReentrantLock, which required code changes.

The fix in Java 25

JEP 491 (delivered in Java 24, included in Java 25) allows virtual threads to acquire, hold, and release monitors independently of carrier threads. The improvement is dramatic:

  • 98% improvement for synchronized blocking operations
  • Legacy code with synchronized works efficiently
  • No need to refactor to ReentrantLock
  • No code changes required

Remaining pinning cases

Pinning still occurs in two scenarios:

  • Native code (JNI) that calls back to Java and blocks
  • Foreign Function calls that block

Detecting pinning issues

Use Java Flight Recorder to diagnose remaining pinning:

$ java -XX:StartFlightRecording=filename=recording.jfr -jar app.jar
$ jfr print --events jdk.VirtualThreadPinned recording.jfr

Deprecations and breaking changes

Security Manager disabled

The Security Manager is permanently disabled in Java 25:

  • System.setSecurityManager() throws UnsupportedOperationException
  • System.getSecurityManager() always returns null
  • Over 50,000 lines of code removed from the JDK

If your code uses Security Manager, test with this flag on Java 17-23 before upgrading:

$ java -Djava.security.manager=disallow -jar app.jar

String templates withdrawn

String templates (JEP 430) were preview in Java 21 but withdrawn in Java 23 due to design issues. Use String.formatted() or String.format() instead.

Finalization deprecated

Classes overriding finalize() generate warnings. Replace with the Cleaner API:

private static final Cleaner CLEANER = Cleaner.create();

public class Resource implements AutoCloseable {
    private final Cleaner.Cleanable cleanable;
    
    public Resource() {
        cleanable = CLEANER.register(this, () -> releaseResource());
    }
    
    @Override
    public void close() {
        cleanable.clean();
    }
}

JNI access warnings

JEP 472 issues warnings when native code is loaded. Applications using native libraries see warnings unless they explicitly opt in:

$ java --enable-native-access=ALL-UNNAMED -jar app.jar

Other removals

  • 32-bit x86 ports — Windows removed in Java 24, Linux in Java 25
  • Non-generational ZGC — Removed in Java 24; generational mode is now the only option
  • sun.misc.Unsafe — Runtime warnings appear by default in Java 24+

Framework compatibility

Spring Boot

Spring Boot versions and Java support:

  • Spring Boot 3.2-3.5 — Java 17 baseline, Java 25 compatible
  • Spring Boot 4.0 — Java 21 baseline, Java 25 recommended

Enable virtual threads in Spring Boot:

# application.properties
spring.threads.virtual.enabled=true

This enables virtual threads for Tomcat, Jetty, task executors, schedulers, Kafka listeners, RabbitMQ listeners, and RestClient.

Recommendation: For new projects targeting Java 25, plan for Spring Boot 4.0 (expected November 2025). Existing Spring Boot 3.x applications run fine on Java 25.

Other frameworks

  • Quarkus 3.x — Java 17 minimum, full Java 25 support
  • Micronaut 4.x — Java 17 minimum, full Java 25 support
  • Jakarta EE 11 — Java 17 minimum, full Java 25 support

Framework startup comparison

JVM startup times on Java 25:

  • Micronaut — 0.656s JVM startup, 0.050s native startup
  • Quarkus — 1.154s JVM startup, 0.049s native startup
  • Spring Boot — ~2-3s JVM startup, ~0.1s native startup

Build tool requirements

Gradle

Gradle versions for Java:

  • Java 21 — Gradle 8.5 minimum
  • Java 25 — Gradle 9.1+ required

Upgrade your Gradle wrapper:

$ ./gradlew wrapper --gradle-version=9.1.0
$ ./gradlew --version

Configure toolchains in build.gradle.kts:

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

Maven

Maven compiler plugin versions for Java:

  • Java 21 — Maven compiler plugin 3.11.0+
  • Java 25 — Maven compiler plugin 3.14.1+

Configure in pom.xml:

<properties>
    <maven.compiler.release>25</maven.compiler.release>
</properties>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.14.1</version>
        </plugin>
    </plugins>
</build>

Common issue: "release version 25 not supported" means your JAVA_HOME points to an older JDK. Fix it with export JAVA_HOME=$(/usr/libexec/java_home -v 25).

IDE support

IDE versions for Java support:

  • IntelliJ IDEA — 2023.3+ for Java 21, 2025.2+ for Java 25
  • Eclipse — 2023-09 + plugin for Java 21, 2025-09 + plugin for Java 25
  • VS Code — Extension Pack for Java 21, Oracle Extension v25+ for Java 25
  • NetBeans — 20+ for Java 21, 27+ for Java 25

Fallback: You can always build from Terminal even if your IDE hasn't caught up with Java 25 support.

Testing framework compatibility

JUnit 5

JUnit 5 fully supports Java 21 and 25. Use JUnit Jupiter 5.10+ for best compatibility.

Mockito

Mockito 5.x is required for Java 21+. Minimum version: Mockito 5.4.0.

Dynamic agent loading triggers warnings in Java 21+. Configure the agent explicitly.

For Maven Surefire:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>
            -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar
        </argLine>
    </configuration>
</plugin>

For Gradle:

test {
    jvmArgs "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('mockito-core') }}"
}

Common issue: "Java 21 (65) is not supported by the current version" means you have an old ByteBuddy version in your dependency tree. Upgrade to the latest Mockito.

When to use each version

Use Java 21 when:

  • Maximum compatibility needed — Broadest framework and library support
  • Conservative environment — Enterprise with slow upgrade cycles
  • Proven stability required — Production systems need battle-tested releases
  • Existing projects — Migration from Java 17 or earlier
  • Spring Boot 3.2-3.5 baseline — Your framework requires Java 21

Use Java 25 when:

  • Starting new projects — Begin with the latest LTS for the longest support runway
  • 8-year support needed — Support extends to 2033 vs 2031 for Java 21
  • Memory optimization matters — 22% heap savings from compact object headers
  • Startup performance is critical — Serverless, edge computing, scale-to-zero scenarios
  • Virtual threads with synchronized code — Pinning problem fully resolved
  • Container deployments — Memory efficiency affects cost and density
  • Spring Boot 4.0 — First-class Java 25 support

Decision summary

Here's how the two versions compare:

Java 21:

  • Released September 2023, mature (2+ years)
  • Free until September 2026 (Oracle)
  • Extended support until September 2031
  • Broad framework support
  • Standard memory efficiency
  • Standard startup speed
  • Virtual threads have pinning issues
  • Mature IDE support
  • Low risk level

Java 25:

  • Released September 2025, new
  • Free until September 2028 (Oracle)
  • Extended support until September 2033
  • Emerging framework support
  • 22% better memory efficiency
  • 2-4x better startup speed (AOT)
  • Virtual thread pinning fixed
  • IntelliJ 2025.2+ required
  • Medium risk level

Recommendation: For new projects starting in late 2025, choose Java 25. The performance improvements are significant, the virtual thread pinning fix removes a major limitation, and you get an extra two years of support. For existing projects on Java 21, upgrade when your framework ecosystem matures—typically 6-12 months after an LTS release.

Migration checklist

If you're upgrading from Java 21 to Java 25, follow this checklist.

Pre-upgrade validation

Run these checks before upgrading:

$ javac -Xlint:deprecation MyApp.java
$ java -Djava.security.manager=disallow -jar app.jar
$ java -XX:StartFlightRecording=filename=recording.jfr -jar app.jar
$ jfr print --events jdk.VirtualThreadPinned recording.jfr

Migration steps

  1. Upgrade build tools — Gradle 9.1+ or Maven compiler plugin 3.14.1+
  2. Update CI images — Switch Docker base images to Java 25
  3. Run full test suite — Unit, integration, and load tests
  4. Test performance features — Compact object headers, AOT cache
  5. Roll out gradually — Canary deployment before full rollout

Breaking changes checklist

  • Security Manager — Test with -Djava.security.manager=disallow, remove Security Manager code
  • JNI warnings — Check for warning messages, add --enable-native-access flag
  • Finalization — Test with -Xlint:deprecation, replace finalize() with Cleaner
  • Mockito agent — Run tests, configure agent explicitly

Troubleshooting

IDE doesn't recognize Java 25: Update to IntelliJ IDEA 2025.2+ or Eclipse 2025-09 with Java 25 plugin. Verify Project SDK and Language Level settings. Build from Terminal as a fallback.

Build tool doesn't support Java 25: For Gradle, upgrade to 9.1+ with ./gradlew wrapper --gradle-version=9.1.0. For Maven, update compiler plugin to 3.14.1+.

"command not found" or wrong Java version: Check your JAVA_HOME with echo $JAVA_HOME. List installed JDKs with /usr/libexec/java_home -V. Set the correct version with export JAVA_HOME=$(/usr/libexec/java_home -v 25).

JNI/native access warnings: Add --enable-native-access=ALL-UNNAMED to JVM arguments.

Homebrew OpenJDK not visible: If you installed via brew install openjdk@25 (formula) instead of brew install --cask temurin@25 (cask), create a symlink:

$ sudo ln -sfn $(brew --prefix)/opt/openjdk@25/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-25.jdk

Docker image architecture mismatch: Specify the platform explicitly:

$ docker run --platform linux/arm64 eclipse-temurin:25-jre java -version

Read next: Fix "java: command not found" for detailed troubleshooting steps.

Quick reference

Here are the essential commands:

# Install Java 21 and Java 25
$ brew install --cask temurin@21
$ brew install --cask temurin@25

# Check version
$ java -version
$ javac -version

# List all installed JDKs
$ /usr/libexec/java_home -V

# Find specific version path
$ /usr/libexec/java_home -v 21
$ /usr/libexec/java_home -v 25

# Set JAVA_HOME (add to ~/.zprofile)
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
export PATH=$JAVA_HOME/bin:$PATH

# Quick switching function (add to ~/.zshrc)
jdk() {
    export JAVA_HOME=$(/usr/libexec/java_home -v"$1")
    java -version
}

# Reload shell config
$ source ~/.zshrc

# Switch versions
$ jdk 21
$ jdk 25

# Test Java 25 simplified syntax
$ echo 'void main() { IO.println("Hello!"); }' > Hello.java
$ java Hello.java

# Enable compact object headers
$ java -XX:+UseCompactObjectHeaders -jar app.jar

# Enable virtual threads in Spring Boot
spring.threads.virtual.enabled=true

Frequently asked questions

Should I use Java 25 or Java 21? Choose Java 25 for new projects. Both are LTS releases, but Java 25 has longer support (until 2033 versus 2031 for Java 21) and includes compact source files, flexible constructors, scoped values, and better performance. Java 21 free updates continue until September 2026.

Is Java 25 stable for production? Yes. As an LTS release, Java 25 underwent extensive testing before release. Oracle, Amazon, Azul, and the Eclipse Foundation all provide production-ready builds. Major frameworks like Spring Boot actively support it.

When should I upgrade to Java 25? Upgrade when your dependencies support it—most modern frameworks already do. For greenfield projects, start with Java 25. For existing projects, test thoroughly in staging first. The one-year overlap with Java 21 free updates gives you time to plan.

What's the difference between JDK and JRE? The JDK (Java Development Kit) includes the compiler (javac) and development tools. The JRE (Java Runtime Environment) only runs Java programs. For development, you need the JDK. Temurin and other modern distributions only provide JDKs.

Does Java 25 support Apple Silicon natively? Yes. All major distributions provide native ARM64 builds for M1, M2, M3, and M4 Macs. These run significantly faster than Intel builds under Rosetta 2 emulation.

What's new with the void main() syntax? Java 25 finalizes compact source files. You can now write void main() { } instead of public static void main(String[] args) { }. The class declaration is optional, and module java.base is automatically imported.

How does the virtual thread pinning fix help? In Java 21, virtual threads pinned to carrier threads inside synchronized blocks, hurting scalability. Java 25 fixes this with a 98% improvement for synchronized blocking operations. No code changes required.

What are compact object headers? JEP 519 reduces object headers from 12 bytes to 8 bytes, saving 10-22% heap space. Enable with -XX:+UseCompactObjectHeaders. Best for applications with many small objects or memory-constrained containers.

What's Next

With Java 21 and Java 25 installed, you're ready to build. Start a new project with your favorite framework, update Maven or Gradle settings to target Java 25, or explore compact source files with a sample application. If you work on multiple projects with different Java versions, see Java Version Managers or Install SDKMAN for Java on Mac for tools that automate switching. If you ever need to remove Java, see Uninstall Java on Mac.

My mac.install.guide is a trusted source of installation guides for professional developers. Take a look at the Mac Install Guide home page for tips and trends and see what to install next.