Lesson 1 of 9 · 20m
The JVM Contract: Bytecode, JIT, and What Java Actually Compiles To
Why Java compiles twice, what the JIT is really doing at warmup, and reading your own bytecode with javap.
You're paged because a service takes thirty seconds to serve its first fast responses after every deploy. Someone shrugs and says "JVM warmup" — which is technically true and completely unhelpful until you know what is actually warming up. This lesson is the mental model that makes that shrug testable.
Java compiles twice
javac compiles .java sources to bytecode in .class files. Bytecode is not machine code — it is an instruction set for a machine that doesn't exist in silicon. When the JVM runs your program it starts out interpreting that bytecode, watches which methods get hot, and hands the hot ones to the JIT compilers (HotSpot's C1 for quick compilation, C2 for aggressive optimization). That's the warmup: your code literally gets faster while it runs.
See it yourself on JDK 17 or 21:
javac Hello.java
javap -c Hello.class
java -XX:+PrintCompilation Hellojavap -c shows the actual bytecode — look for invokevirtual, getstatic, invokedynamic. PrintCompilation prints methods as the JIT picks them up.
The contract this buys you: one compiled artifact runs on x86 Linux, ARM Macs, containers, mainframes — the JVM absorbs the platform differences. The enterprise bet on Java wasn't the language; it was this contract plus a runtime that can keep a process alive for years.
What's actually in the box
- JDK = compiler (
javac), diagnostic tools (jcmd,jstat,javap), and the JVM. - JRE = just the runtime. Since Java 11 there's no separate JRE download; build a minimal runtime with
jlinkinstead. - LTS cadence: 8, 11, 17, and 21 are the long-term-support versions most enterprises standardize on. Everything in this course assumes 17+.
java -version tells you exactly which runtime you're on — check it before believing any tutorial.
The bug that ships: CPU and memory blindness in containers
Old JVMs sized the heap off the physical machine's RAM, ignoring cgroup limits, and got OOM-killed inside their own containers. Since JDK 8u191 — and all of 11+ — the JVM is container-aware, but your flags still matter. In Kubernetes, prefer a percentage over a fixed -Xmx so a memory-limit bump actually reaches the JVM:
java -XX:MaxRAMPercentage=75.0 -jar app.jarTry this
Take any class, run javap -c on it, and find one invokedynamic — it's how lambdas and string concatenation get wired. Then run a loop-heavy program with -XX:+PrintCompilation and watch method names scroll past. You've just watched the JIT decide your code is worth optimizing.
Field notes
- Warmup is why short load tests lie: a 30-second benchmark measures interpretation, not steady state. Ramp for minutes, not seconds.
- An
-Xmxsmaller than your live data set doesn't fail fast — it fails slow, as GC thrash. Size it deliberately. - Bytecode stability is why Java tooling (profilers, agents, instrumenters) is better than almost any other ecosystem's — they all hook the same
.classformat.