Home › Programming languages › Managed & backend
guide low

Java and Kotlin

Problem Is the JVM, with Java or Kotlin, the right platform for a large long-running backend, data infrastructure or Android app? This guide covers its strengths and where its startup and memory cost hurts.

The JVM: near-native speed after JIT warmup, low-pause GCs, virtual threads and the deepest enterprise ecosystem; Kotlin is the nicer language on top.

javakotlinjvmjitgcvirtual-threadscoroutinesandroid
Overview

The JVM profiles running bytecode and JIT-compiles hot code, inlining across virtual calls based on what actually runs; once warm, throughput is close to C++ for many server workloads. Modern collectors handle multi-GB heaps with millisecond pauses, and virtual threads (JDK 21) make blocking code scale like async. Kotlin compiles to the same bytecode with null safety and coroutines.

Key facts

Use when

  • Large long-lived backends, data infrastructure (Kafka, Spark, Flink), Android (Kotlin).
  • Real uses by field: mobile (Kotlin is used by over 60 % of professional Android developers, per Android Developers), financial exchanges (the LMAX trading platform runs a JVM business logic processor handling 6 million orders per second on one thread), event streaming (Apache Kafka needs Java 17+).

Avoid when

  • CLIs, short-lived serverless functions, or containers under 128 MiB.

Deep dive

Decision table

JobPickWhy
Enterprise web servicesSpring Boot (Java or Kotlin)Largest ecosystem, hiring
Lean microservices, fast startupQuarkus or Micronaut, optionally native-imageBuild-time DI, small footprint
High-concurrency blocking I/OVirtual threads (JDK 21+)Thread-per-request without thread exhaustion
Android appsKotlin + Jetpack ComposeGoogle's primary language
Shared mobile logic (iOS + Android)Kotlin MultiplatformShare domain code, native UIs
Async-heavy Kotlin backendKtor + coroutinesStructured concurrency
Low-latency tradingJava with off-heap memory, ZGC, careful allocationProven (LMAX-style) but expert work
Data/stream processingKafka, Flink, Spark on the JVMThe platforms are JVM-native
Build toolGradle (Kotlin DSL) or MavenMaven 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=75 rather than fixed -Xmx guesses.

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: suspend functions compiled to state machines; coroutineScope gives 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 synchronized blocks on JDK 21-23 (fixed in JDK 24); prefer ReentrantLock on older JDKs, and don't pool virtual threads.
  • ThreadLocal caches 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 Stream pipelines on tiny collections.
  • Kotlin !! and platform types from Java APIs hiding nulls; GlobalScope.launch leaking coroutines.
  • Blocking calls inside Dispatchers.Default coroutines; use Dispatchers.IO or suspend-aware clients.
  • Spring "magic" (proxies, reflection) slowing startup and hiding behavior; keep configuration explicit.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
OpenJDK (HotSpot)Java / C++openjdk/jdkbrew install openjdk————reference JDK; builds from other vendors exist
Kotlin compilerKotlinJetBrains/kotlinbrew install kotlin————K2 compiler since 2.0
GradleJava / Kotlingradle/gradlebrew install gradle————build tool, Kotlin DSL
Apache MavenJavaapache/mavenbrew install maven————convention-based build tool
Spring BootJavaspring-projects/spring-bootMaven/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

Alternatives and related