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[] argsrequired - java.lang.IO — New console I/O class with
println()andreadln()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
synchronizedworks 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()throwsUnsupportedOperationExceptionSystem.getSecurityManager()always returnsnull- 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
- Upgrade build tools — Gradle 9.1+ or Maven compiler plugin 3.14.1+
- Update CI images — Switch Docker base images to Java 25
- Run full test suite — Unit, integration, and load tests
- Test performance features — Compact object headers, AOT cache
- 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-accessflag - Finalization — Test with
-Xlint:deprecation, replacefinalize()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.