JDK 23’s Position
JDK 23, released on September 17, 2024, is a non-LTS release but one that made meaningful progress in both developer experience and GC performance. In particular, the finalization of Markdown Documentation Comments (JEP 467) marks a fundamental change in how Javadoc has been written for over 20 years.
This article covers JDK 23’s key JEPs with runnable examples.
Markdown Documentation Comments (JEP 467)
Traditional Javadoc was HTML tag-based. Writing a simple list required <ul> and <li> tags, and code blocks needed cumbersome syntax like <pre>{@code ...}</pre>. Starting with JDK 23, Javadoc comments can be written in Markdown.
Markdown Javadoc uses /// (three slashes) at the start of comments, distinguishing it from the traditional /** ... */ block comments.
import java.util.List;
import java.util.stream.Collectors;
/// A utility class for processing name lists.
///
/// ## Key Features
/// - Name filtering (by length)
/// - Uppercase conversion
/// - Sorting
///
/// ## Usage Example
/// ```
/// List<String> result = NameProcessor.filterAndSort(names, 3);
/// ```
///
/// @param names the list of names to process
/// @param minLength minimum character count
/// @return filtered and sorted list of names
public class NameProcessor {
/// Filters names that meet the minimum length and returns a sorted list.
///
/// **Note**: `null` elements are automatically excluded.
public static List<String> filterAndSort(List<String> names, int minLength) {
return names.stream()
.filter(name -> name != null && name.length() >= minLength)
.sorted()
.collect(Collectors.toList());
}
public static void main(String[] args) {
// Markdown Javadoc feature test
List<String> names = List.of("Jo", "Alexander", "Elizabeth", "Al", "Christopher");
List<String> result = filterAndSort(names, 5);
System.out.println("Names with 5+ chars: " + result);
// Output: Names with 5+ chars: [Alexander, Christopher, Elizabeth]
List<String> all = filterAndSort(names, 1);
System.out.println("All names (sorted): " + all);
// Output: All names (sorted): [Al, Alexander, Christopher, Elizabeth, Jo]
}
}
The key benefit of Markdown Javadoc is readability. Natural documentation can be written without HTML tags, and it renders directly in GitHub and IDE previews. Existing Javadoc tags like @param and @return can still be used, maintaining compatibility with existing workflows.
ZGC Generational Mode by Default (JEP 474)
ZGC is a low-latency GC officially introduced in JDK 15. JDK 21 added Generational mode, and JDK 23 made it the default.
Generational GC is based on the Weak Generational Hypothesis — “most objects die young.” Think of a supermarket’s fresh food section: it’s more efficient to check items with short shelf life (Young objects) frequently and inspect long-storage items (Old objects) occasionally.
import java.util.ArrayList;
import java.util.List;
public class ZgcGenerationalDemo {
public static void main(String[] args) {
// Scenario demonstrating ZGC Generational mode effect
// Run: java -XX:+UseZGC ZgcGenerationalDemo
// Young generation: most objects are created and quickly reclaimed here
long startMemory = Runtime.getRuntime().freeMemory();
// Create many short-lived objects (Young generation target)
for (int i = 0; i < 1_000_000; i++) {
String temp = "temp-object-" + i; // Immediately dereferenced -> reclaimed in Young GC
}
long afterYoung = Runtime.getRuntime().freeMemory();
// Long-lived objects: promoted to Old generation
List<String> longLived = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
longLived.add("long-lived-" + i); // Reference maintained -> promoted to Old generation
}
long afterOld = Runtime.getRuntime().freeMemory();
System.out.println("=== ZGC Generational Mode Simulation ===");
System.out.println("Initial free memory: " + formatBytes(startMemory));
System.out.println("After Young objects: " + formatBytes(afterYoung));
System.out.println("Old objects retained: " + formatBytes(afterOld));
System.out.println("Long-lived object count: " + longLived.size());
// Example output:
// === ZGC Generational Mode Simulation ===
// Initial free memory: 245.8 MB
// After Young objects: 198.3 MB
// Old objects retained: 196.1 MB
// Long-lived object count: 10000
}
static String formatBytes(long bytes) {
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
}
}
Before JDK 23, the -XX:+ZGenerational flag was needed to use generational mode. From JDK 23, just specifying -XX:+UseZGC automatically applies generational mode. In terms of performance numbers, approximately 10% throughput improvement and 10-20% P99 pause time improvement have been reported.
Module Import Declarations (JEP 476, Preview)
Instead of importing packages with import java.util.*, this feature allows importing an entire module with a single line. It was introduced as a preview in JDK 23.
// JDK 23 Preview feature — compile: javac --enable-preview --source 23 ModuleImportDemo.java
// import module java.base; // One line to use all packages from java.base module
// Since it's a preview feature, the equivalent using traditional imports is shown
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ModuleImportDemo {
public static void main(String[] args) {
// When Module Import is finalized, the 3 imports above become one line:
// import module java.base;
// Module import effect: java.util, java.util.stream, java.io, etc. all available
List<String> frameworks = List.of("Spring", "Quarkus", "Micronaut", "Helidon");
Map<Integer, List<String>> grouped = frameworks.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("Frameworks (grouped by name length):");
grouped.forEach((len, names) ->
System.out.println(" " + len + " chars: " + names));
// Output:
// Frameworks (grouped by name length):
// 6 chars: [Spring]
// 7 chars: [Quarkus, Helidon]
// 10 chars: [Micronaut]
}
}
Having 10 or 20 import lines in a Java project is common. When Module Import is finalized, a single import module java.base; line covers java.util, java.io, java.time, and other base packages, drastically reducing boilerplate.
Other Notable JEPs
Stream Gatherers (JEP 473, Second Preview): The gather() method that adds user-defined intermediate operations to the Stream API. It enables implementing window processing, stateful transformations, and more that are hard to express with map, filter, flatMap. This feature was finalized in JDK 24.
Implicitly Declared Classes (JEP 477, Third Preview): A feature for writing just void main() without class declarations. The goal is to lower Java’s entry barrier for education or scripting purposes. Finalized in JDK 25.
sun.misc.Unsafe Memory Access Methods Deprecation (JEP 471): Although warnings to “not use this” have been present for a long time, the memory access methods of sun.misc.Unsafe — which many libraries have used for performance — were officially deprecated. The alternative is the Foreign Function and Memory API (java.lang.foreign) finalized in JDK 22.
JDK 23 Performance Summary
| Item | Change |
|---|---|
| ZGC throughput | ~10% improvement with generational default |
| ZGC P99 pause | 10-20% improvement |
| ZGC mode | Generational is default, non-generational maintained |
Summary
| Feature | Status | Key Points |
|---|---|---|
| Markdown Javadoc | Final | /// syntax for Markdown documentation comments |
| ZGC Generational Default | Final | Throughput 10% up, P99 pause 10-20% down |
| Module Import | Preview | import module java.base; for entire module import |
| Stream Gatherers | 2nd Preview | User-defined Stream intermediate operations |
| Implicitly Declared Classes | 3rd Preview | Write void main() without class declaration |
| Unsafe Deprecation | Final | sun.misc.Unsafe memory methods deprecated |
JDK 23 is non-LTS but brings many changes with direct practical impact. Markdown Javadoc can change documentation writing habits, and ZGC Generational default provides performance improvements without additional tuning. Module Import and Stream Gatherers, which appeared as previews, have been finalized in later versions — getting familiar with their syntax early is recommended.