A complete, easy-to-understand guide to how Java & Kotlin really work: the JVM, memory management,
garbage collection, compilers (javac, JIT, AOT, GraalVM, kotlinc), JDK distributions, customizing
the JDK, and the type systems — plus version-by-version feature tables and a deep Q&A bank.
Both Java and Kotlin compile to the same target: JVM bytecode (.class files), which runs on the
JVM (Java Virtual Machine). That’s why they interoperate seamlessly.
Kotlin source (.kt) ─kotlinc─┘ (interprets + compiles hot code)
Key idea — “Write once, run anywhere”: bytecode is platform-independent. The JVM for each OS
turns it into native instructions. The JVM also does the heavy lifting at runtime: memory
management, garbage collection, JIT compilation, and security.
Senior framing: “Java/Kotlin are managed languages — you don’t malloc/free. The JVM
owns memory and reclaims it via GC, and it gets fast by JIT-compiling hot code paths to native at
runtime based on real profiling data.”
The JVM has three main subsystems:
Class Loader — finds, loads, links, and initializes .class files.
Runtime Data Areas — the memory (heap, stacks, metaspace…) — see §2.
An object is eligible for GC when it’s no longer reachable from any “GC root” (active stack
frames, static fields, etc.). Reference types control this:
Strong (normal): never collected while reachable.
Soft: collected only when memory is low (good for caches).
Weak: collected at the next GC (e.g., WeakHashMap).
Yes, they still happen — when you keep references you don’t need:
Forgotten entries in a static collection / cache that grows forever.
Unremoved listeners/callbacks.
ThreadLocals not cleared in thread pools.
Inner classes holding the outer instance.
“How do you find a memory leak?” Watch heap growth + GC frequency; take a heap dump
(jmap, or -XX:+HeapDumpOnOutOfMemoryError); analyze with Eclipse MAT / VisualVM to find the
dominator tree / what holds the references; fix the retained reference.
GC automatically frees heap memory occupied by unreachable objects. The core algorithm is
mark-and-sweep (mark reachable objects, sweep the rest), usually with compaction (move
survivors together to avoid fragmentation) and generational collection (collect young gen often,
old gen rarely).
The fundamental trade-off:throughput (total work done) vs latency (pause times) vs
footprint (memory used). No GC wins all three — you pick.
Stop-the-world (STW): the GC pauses all application threads. Concurrent collectors do most
work while the app runs to minimize these pauses.
G1 splits the heap into many equal regions and collects the ones with the most garbage
“first,” aiming for a configurable max pause (-XX:MaxGCPauseMillis).
ZGC / Shenandoah are the modern ultra-low-latency collectors — concurrent marking and
compaction, so pauses stay sub-millisecond even on multi-terabyte heaps.
“Default to G1. If you need consistently tiny pauses on a big heap, use ZGC. Use
Parallel for batch/throughput jobs that don’t care about pauses. Always measure before tuning.”
The JVM starts by interpreting bytecode (fast startup), and profiles which methods run a lot
(“hot”). Hot methods get JIT-compiled to native machine code for speed. This is tiered
compilation:
C1 (client compiler): compiles quickly, lighter optimizations → fast warm-up.
C2 (server compiler): slower to compile but heavy optimizations → peak performance.
Code flows: interpreter → C1 → C2 as a method gets hotter.
JIT optimizations (why long-running JVMs get faster over time): method inlining, dead-code
elimination, escape analysis (stack-allocate objects that never escape a method), loop unrolling,
and deoptimization (revert if an assumption breaks).
“The JVM is fast despite bytecode because the JIT compiles hot paths to optimized native code
using real runtime profiling — something an ahead-of-time compiler can’t do as well. The cost is
warm-up time.”
For fast startup / low memory (serverless, CLIs, microservices), you can compile ahead-of-time
to a native executable with GraalVM Native Image.
Pros: millisecond startup, low memory, no JVM needed at runtime.
Cons: longer build, closed-world assumption (reflection/dynamic loading need config),
generally lower peak throughput than a warmed-up JIT.
JIT (normal JVM)
AOT / Native Image
Startup
Slow (warm-up)
Instant
Peak performance
Higher (runtime profiling)
Lower
Memory
Higher
Lower
Best for
Long-running servers
Serverless, CLIs, short-lived
C1/C2 vs Graal: GraalVM also ships a Graal JIT (a modern compiler written in Java) that can
replace C2, and the Native Image AOT tool. Two different things under one brand.
Kotlin is a multi-target language. The same language compiles to several backends:
Target
Tool
Output
Use
Kotlin/JVM
kotlinc
JVM bytecode (.class)
Backend, Android, anything on the JVM
Kotlin/Native
kotlinc-native (LLVM)
Native binary, no VM
iOS, embedded, desktop
Kotlin/JS
Kotlin/JS compiler
JavaScript
Web frontends
Kotlin Multiplatform (KMP)
shared modules
per-platform
Share business logic across iOS/Android/web
The modern compiler frontend is K2 (faster, better type inference; stable in Kotlin 2.0).
On the JVM, Kotlin produces standard bytecode, so it runs on the same JVM, uses the same GC and
JIT, and interoperates with Java both ways (call Java from Kotlin and vice versa).
“How does Kotlin interop with Java?” Same bytecode + JVM. Kotlin maps Java types, exposes
annotations (@JvmStatic, @JvmOverloads, @JvmName) to shape the Java-facing API, and treats
Java types as “platform types” (nullability unknown) at the boundary.
JRE (Java Runtime Environment): JVM + standard libraries — enough to run Java apps. (Standalone
JREs were dropped after Java 8; now you ship a runtime via jlink.)
JDK (Java Development Kit): JRE + tools to develop (compile, debug, package).
OpenJDK is the open-source reference; many vendors ship builds of it:
Distribution
Notes
OpenJDK
The open-source reference implementation
Oracle JDK
Oracle’s build; commercial terms for some uses
Eclipse Temurin (Adoptium)
Popular free, well-tested OpenJDK build
Amazon Corretto
Free, long-term support, AWS-tuned
Azul Zulu / Zing
Zulu free; Zing has the C4 pauseless GC
GraalVM
OpenJDK + Graal JIT + Native Image (AOT)
Red Hat / Microsoft / SAP builds
Vendor-supported OpenJDK builds
“They’re all OpenJDK under the hood and pass the same TCK compatibility tests, so they’re
functionally equivalent. You choose based on support, license, LTS cadence, and special features
(e.g., GraalVM native image, Azul’s pauseless GC).”
LTS (Long-Term Support) versions — the ones companies standardize on: 8, 11, 17, 21 (and 25
next). Non-LTS releases (every 6 months) are for trying new features.
Why it matters: smaller container images, less attack surface, faster startup. A “hello world”
runtime can shrink from ~300 MB to ~30–40 MB. This is the answer to “how do you customize/slim the
JDK.”
“How would you reduce a Java container’s size & startup?” Use jdeps → jlink for a minimal
runtime (or GraalVM native image for serverless), set -Xms=-Xmx, choose an appropriate GC (Serial/
G1 for small heaps), and use a slim base image. Mention container-awareness (the JVM respects cgroup
limits since Java 10+).
Generics give compile-time type safety: List<String>. But the JVM uses type erasure — generic
type info is removed at runtime (List<String> and List<Integer> are both just List).
Consequence: you can’t do new T[], instanceof List<String>, or overload by generic type.
Why: backward compatibility with pre-generics bytecode.
Lightweight “suspendable” computations for async code that reads like sequential code:
suspendfunloadUser(): User =withContext(Dispatchers.IO) { api.fetch() }
Not threads: millions of coroutines can run on a small thread pool. The compiler transforms
suspend functions into a state machine (continuation-passing style) — suspension points don’t
block the underlying thread.
Structured concurrency: scopes (coroutineScope, viewModelScope) tie coroutine lifetimes to
their parent, so they’re cancelled together — no leaks.
“Coroutines vs threads?” Threads are OS-level and heavy (~1 MB stack each). Coroutines are a
language/library construct — cheap, scheduled cooperatively onto threads, suspend instead of block.
Ideal for high-concurrency I/O. (Java’s answer is Virtual Threads / Project Loom, Java 21.)
VisualVM / JDK Mission Control + JFR — profiling, allocation hot spots.
Eclipse MAT — analyze heap dumps for leaks (dominator tree, retained size).
-Xlog:gc* — GC logs to understand pause causes & frequency.
Common tuning moves:
Set -Xms = -Xmx in production (avoid heap resizing jitter).
Pick the GC for your goal (G1 default; ZGC for low pause; Parallel for throughput).
Reduce allocations in hot paths (object pooling sparingly; avoid boxing; reuse buffers).
Right-size thread pools; for high-concurrency I/O, use virtual threads (Java 21) or coroutines
(Kotlin).
Watch for leaks: growing old gen + frequent Full GCs = trouble.
“Measure first. Most JVM performance problems are allocation pressure (too much garbage) or
a bad GC choice for the workload, not raw CPU. Profile, then change one thing.”
Stack is per-thread, holds local variables/method frames, fast LIFO, freed on method return,
overflows with deep recursion (StackOverflowError). Heap is shared, holds all objects, managed by
GC, errors with OutOfMemoryError.
Q: How does garbage collection work?
Mark reachable objects from GC roots, sweep the unreachable, often compact survivors. It’s
generational: young gen (Eden+survivors) collected often and cheaply (minor GC); old gen holds
long-lived objects, collected rarely (major/full GC).
Q: G1 vs ZGC vs Parallel?
Parallel = max throughput, longer STW pauses (batch). G1 = balanced, region-based, default, targets
a max pause. ZGC = concurrent, sub-millisecond pauses on huge heaps for low-latency services.
Q: Can you have a memory leak in Java? How do you find it?
Yes — by retaining references you no longer need (static caches, unremoved listeners, ThreadLocals).
Find it via heap growth/GC monitoring, a heap dump (jmap / HeapDumpOnOOM), analyzed in Eclipse MAT to
see what retains the objects.
Q: javac vs JIT?
javac compiles source to portable bytecode ahead of time. The JIT compiles hot bytecode to native
machine code at runtime using profiling (tiered C1→C2). That’s why the JVM warms up and then runs
fast.
Q: What is GraalVM Native Image and its trade-offs?
An AOT compiler producing a standalone native binary — instant startup, low memory, no JVM. Costs:
longer builds, closed-world (reflection needs config), and usually lower peak throughput than a
warmed JIT. Great for serverless/CLIs.
jdeps to find needed modules, then jlink to build a minimal custom runtime with only those
modules — much smaller images and attack surface. Or GraalVM native image to drop the JVM entirely.
Q: What is type erasure?
Generics are compile-time only; the JVM removes generic type info at runtime for backward
compatibility, so List<String> and List<Integer> are both just List. Hence no new T[] or
generic instanceof.
Q: Records vs Lombok vs Kotlin data classes?
All reduce boilerplate for data holders. Java records (16+) are built-in, immutable, auto-generate
accessors/equals/hashCode/toString. Kotlin data classes do the same (plus copy) and predate them.
Q: How do Kotlin coroutines work under the hood?
The compiler transforms suspend functions into a state machine (continuation-passing). Suspension
points free the underlying thread instead of blocking, so millions of coroutines multiplex onto a
small thread pool — cheap structured concurrency.
Q: Coroutines vs Java virtual threads?
Both enable massive concurrency cheaply. Coroutines are a compiler/library feature with suspend
semantics and structured concurrency. Virtual threads (Java 21/Loom) are JVM-level lightweight
threads that look like normal blocking code but don’t pin an OS thread — simpler interop with
existing blocking APIs.
Q: Why is Kotlin null safety better than Java’s?
It’s enforced by the type system at compile time (String vs String?), with safe calls ?.,
elvis ?:, and smart casts — eliminating most NPEs before runtime, versus Java’s runtime NPEs (or
Optional/annotations bolted on).
Q: What’s metaspace and how is it different from PermGen?
Metaspace (Java 8+) stores class metadata in native memory and auto-grows (bounded by
-XX:MaxMetaspaceSize), replacing the fixed-size heap-based PermGen that caused
OutOfMemoryError: PermGen space.
Q: What is escape analysis?
A JIT optimization: if an object never “escapes” a method (no outside reference), the JVM can
allocate it on the stack or eliminate it, reducing heap allocation and GC pressure.
Records/sealed/var/pattern matching = modern Java; null safety + coroutines + data classes
= Kotlin highlights.
Concurrency at scale: virtual threads (Java 21) / coroutines (Kotlin).
Memory leaks happen via retained references; find with heap dumps + MAT.
Tune by measuring: JFR/VisualVM/jstat; set -Xms=-Xmx; cut allocations.
End of handbook. Master the four pillars — memory model, GC, the two-stage compilation, and JDK
customization — and you’ll handle any Java/Kotlin internals interview.