Deep dive
Decision table
| Job | Pick | Why |
|---|---|---|
| Enterprise web services | Spring Boot (Java or Kotlin) | Largest ecosystem, hiring |
| Lean microservices, fast startup | Quarkus or Micronaut, optionally native-image | Build-time DI, small footprint |
| High-concurrency blocking I/O | Virtual threads (JDK 21+) | Thread-per-request without thread exhaustion |
| Android apps | Kotlin + Jetpack Compose | Google's primary language |
| Shared mobile logic (iOS + Android) | Kotlin Multiplatform | Share domain code, native UIs |
| Async-heavy Kotlin backend | Ktor + coroutines | Structured concurrency |
| Low-latency trading | Java with off-heap memory, ZGC, careful allocation | Proven (LMAX-style) but expert work |
| Data/stream processing | Kafka, Flink, Spark on the JVM | The platforms are JVM-native |
| Build tool | Gradle (Kotlin DSL) or Maven | Maven for simplicity, Gradle for flexibility |
Memory and performance model
- Everything except primitives is a heap object behind a reference;
List<Integer>boxes each element. Primitive arrays and value-like records reduce pressure; Project Valhalla (value classes) is still in progress. - Tiered compilation: interpreter → C1 → C2 with profile-guided inlining, escape analysis (scalar replacement), deoptimization when assumptions break.
- GC choice: G1 (default, balanced, pause target 200 ms), ZGC (
-XX:+UseZGC, sub-ms pauses, more CPU), Parallel (batch throughput), Shenandoah (low pause, in some distributions). - Containers: the JVM reads cgroup limits; size the heap with
-XX:MaxRAMPercentage=75rather than fixed-Xmxguesses.
Concurrency model
- Platform threads map 1:1 to OS threads (about 1 MB stack reserved each); virtual threads are scheduled by the JVM on a ForkJoinPool carrier set and unmount at blocking calls, so millions are possible.
java.util.concurrent: executors,ConcurrentHashMap,CompletableFuture, locks, atomics, and a well-specified memory model (JSR-133).- Kotlin coroutines:
suspendfunctions compiled to state machines;coroutineScopegives structured concurrency (children cancel with the parent); dispatchers choose threads.
Code
Java 21+: virtual threads with plain blocking code (java Fetch.java).
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Fetch {
record Result(String url, int status, long millis) {}
public static void main(String[] args) throws Exception {
var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
List<String> urls = List.of("https://example.com", "https://example.org");
try (var exec = Executors.newVirtualThreadPerTaskExecutor()) { // one cheap thread per task
List<Future<Result>> futures = urls.stream().map(u -> exec.submit(() -> {
long t0 = System.nanoTime();
var req = HttpRequest.newBuilder(URI.create(u)).timeout(Duration.ofSeconds(10)).build();
var resp = client.send(req, HttpResponse.BodyHandlers.discarding()); // blocking is fine
return new Result(u, resp.statusCode(), (System.nanoTime() - t0) / 1_000_000);
})).toList();
for (var f : futures) System.out.println(f.get());
} // close() waits for all tasks
}
}
Kotlin: structured concurrency with coroutines (kotlinx-coroutines-core).
import kotlinx.coroutines.*
data class Quote(val symbol: String, val price: Double)
suspend fun fetchQuote(symbol: String): Quote {
delay(100) // suspends without blocking a thread
return Quote(symbol, 42.0)
}
fun main() = runBlocking {
val quotes = withTimeout(2_000) { // cancels all children on timeout
listOf("AAPL", "MSFT", "NVDA")
.map { s -> async(Dispatchers.IO) { fetchQuote(s) } }
.awaitAll() // one failure cancels the siblings
}
quotes.forEach(::println)
}
Container JVM flags:
java -XX:+UseZGC -XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError \
-Xlog:gc*:file=/tmp/gc.log:time,uptime -jar app.jar
Efficiency
- Cost: a JVM pays startup and warmup before it is fast: Spring PetClinic loads about 21,000 classes and starts in 4.5 s on JDK 23, 2.6 s on JDK 24 with an AOT cache (JEP 483). Generational ZGC pauses are typically under 1 ms, where G1 pauses range from milliseconds to seconds (JEP 439). Every non-primitive is a heap object; compact object headers cut SPECjbb2015 heap use by 22 % (JEP 519). Bound: warmup and allocation rate; after warmup, memory bandwidth.
- Levers:
- AOT class loading and linking cache — startup 42 % lower on PetClinic and a small test program (JEP 483) — JDK 24+, servers with long startup
- compact object headers — 22 % less heap and 8 % less CPU on SPECjbb2015 (JEP 519) — JDK 25+, many small objects
- ZGC (
-XX:+UseZGC) — pauses under 1 ms at the cost of CPU and memory headroom (JEP 439) — latency-sensitive services - virtual threads — thread-per-request style with one cheap thread per task, no pool tuning (JEP 444) — blocking I/O workloads, not CPU-bound ones
- primitive arrays and records instead of boxed collections — fewer objects for the GC (order of) — hot data paths
- Naive → standard → state of the art:
- cold JVM, default GC, boxed collections · seconds of warmup, 4.5 s start on PetClinic
- warmed service, tuned heap with
MaxRAMPercentage· steady-state throughput - AOT cache, compact headers, ZGC where pauses matter · 2.6 s start, 22 % less heap
Build and deploy
- Fat JAR or layered container images (Jib, Spring Boot buildpacks); pin JDK version in the image.
- Faster startup: Class Data Sharing / AOT cache (Project Leyden, JDK 24+), CRaC checkpoints, or GraalVM native-image (needs reflection config; some libraries resist).
- Profiling in production: Java Flight Recorder (
-XX:StartFlightRecording), async-profiler. - Kotlin: Gradle Kotlin DSL; K2 compiler (Kotlin 2.0+) speeds builds.
Pitfalls
- Benchmarking cold JVMs; use JMH for microbenchmarks and warm up services before load tests.
- Virtual threads pinned by
synchronizedblocks on JDK 21-23 (fixed in JDK 24); preferReentrantLockon older JDKs, and don't pool virtual threads. ThreadLocalcaches with millions of virtual threads: memory explodes.- Heap sized to the container limit: native memory (metaspace, threads, direct buffers) pushes the pod over and it gets OOM-killed.
- Autoboxing in hot loops and
Streampipelines on tiny collections. - Kotlin
!!and platform types from Java APIs hiding nulls;GlobalScope.launchleaking coroutines. - Blocking calls inside
Dispatchers.Defaultcoroutines; useDispatchers.IOor suspend-aware clients. - Spring "magic" (proxies, reflection) slowing startup and hiding behavior; keep configuration explicit.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| OpenJDK (HotSpot) | Java / C++ | openjdk/jdk | brew install openjdk | — | — | — | — | reference JDK; builds from other vendors exist |
| Kotlin compiler | Kotlin | JetBrains/kotlin | brew install kotlin | — | — | — | — | K2 compiler since 2.0 |
| Gradle | Java / Kotlin | gradle/gradle | brew install gradle | — | — | — | — | build tool, Kotlin DSL |
| Apache Maven | Java | apache/maven | brew install maven | — | — | — | — | convention-based build tool |
| Spring Boot | Java | spring-projects/spring-boot | Maven/Gradle dependency (start.spring.io) | — | — | — | — | web service framework |
Sources
- OpenJDK, JEP 444: Virtual Threads spec; JEP 439: Generational ZGC spec; JEP 483: Ahead-of-Time Class Loading & Linking spec; JEP 491: Synchronize Virtual Threads without Pinning spec; JEP 519: Compact Object Headers spec.
- Goetz et al., Java Concurrency in Practice, Addison-Wesley, 2006.
- JetBrains, Kotlin documentation: Coroutines guide. docs
- Oracle, HotSpot Virtual Machine Garbage Collection Tuning Guide. docs
- Fowler, The LMAX Architecture, 2011. post
- Android Developers, Kotlin and Android. docs · Apache Kafka, Quickstart. docs