Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 4

    JDK vs JRE vs JVM

    Every production incident involving "Java is slow," "OutOfMemoryError," or "ClassNotFoundException" eventually traces back to the same platform layer: JDK, JRE, and JVM.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Every production incident involving "Java is slow," "OutOfMemoryError," or "ClassNotFoundException" eventually traces back to the same platform layer: JDK, JRE, and JVM. These three terms are not interchangeable — confusing them leads to wrong Docker images, bloated CI pipelines, and architecture reviews that miss the real runtime boundary.

    The JDK (Java Development Kit) is what developers install: compiler (javac), debugger (jdb), profiler (jcmd, JFR), and the full standard library. The JRE (Java Runtime Environment) was historically "JVM + libraries to run apps" — since Java 11, Oracle no longer ships a standalone JRE; production images use modular JRE builds (e.g. jlink, Temurin JRE). The JVM (Java Virtual Machine) is the engine: class loading, bytecode verification, interpreter, JIT compiler, garbage collector, and native thread management.

    Staff engineers treat the JVM as a managed runtime platform — not a black box. Understanding its internals is how you debug Metaspace leaks at Goldman Sachs, tune GC pauses on Netflix payment services, and right-size Kubernetes pods at Amazon.

    Business problem

    Enterprise teams pay when JDK/JRE/JVM boundaries are misunderstood:

    • Wrong container images: Shipping full JDK (500MB+) to production exposes compiler tools, increases attack surface, and wastes registry bandwidth — when a slim JRE suffices.
    • Classloader incidents: Spring Boot fat JARs with duplicate dependencies cause NoClassDefFoundError at 2 AM — root cause is classloader hierarchy, not "random Java bug."
    • JIT warm-up blindness: Autoscaling on CPU without readiness probes deploys cold JVMs into traffic — p99 latency spikes for 30–90 seconds after every scale-out event.
    • Compliance failures: Running EOL JRE without vendor patches (Temurin/Corretto) fails PCI-DSS and SOC2 audits — the JVM is part of your supply chain.

    Why this topic exists

    Java separates compile-time from run-time deliberately — this is the architectural bet that enables portability and optimization:

    • Compile once: javac produces bytecode — not native machine code tied to one CPU/OS.
    • Run anywhere: Each platform ships a JVM that interprets/JITs bytecode to native instructions.
    • Optimize at runtime: JIT sees actual hot paths — static AOT compilers cannot match profile-guided optimization on long-lived servers.
    • Sandbox execution: Bytecode verifier rejects unsafe instructions before they execute — foundational to enterprise trust in downloaded JARs.

    Core concepts

    JDK vs JRE vs JVM — the definitive breakdown:

    • JDK = JVM + development tools + full API modules. Includes javac, jar, javadoc, jshell, jfr, jlink, jdeps.
    • JRE = JVM + runtime libraries needed to execute compiled apps. Deprecated as standalone product; use jlink custom runtime or vendor JRE images.
    • JVM = Execution engine only. Subsystems: class loader, bytecode verifier, interpreter, JIT (C1/C2), GC, native interface (JNI).
    • Bytecode = Platform-independent opcodes in .class files (defined by JVM Specification). Executed by interpreter; hot methods JIT-compiled.
    • Class loading = Bootstrap → Platform (Extension) → Application class loaders. Delegation model: parent-first by default.
    • JIT = Just-In-Time compiler transforms hot bytecode to native code. C1 (client/quick), C2 (server/aggressive), Tiered compilation on modern HotSpot.

    Internal architecture

    Full Java platform architecture — how JDK, JRE, and JVM nest inside a production deployment:

    text
    ┌──────────────────────────────── JDK (Development) ────────────────────────────────┐
    │ Developer Tools: javac · jar · javadoc · jshell · jcmd · jfr · jlink · jdeps │
    ├──────────────────────────────── Runtime Libraries ──────────────────────────────┤
    │ java.base · java.logging · java.net.http · java.sql · java.xml · ... (modules) │
    ├────────────────────────────── JVM Engine ─────────────────────────────────────────┤
    │ │
    │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
    │ │Class Loaders│──▶│ Verifier │──▶│ Interpreter │──▶│ JIT (C1 → C2) │ │
    │ │ Boot/Plat/ │ │ Stack map │ │ Cold path │ │ HotSpot native code│ │
    │ │ Application │ │ Safety check │ │ execution │ │ Profile-guided │ │
    │ └─────────────┘ └──────────────┘ └─────────────┘ └─────────────────────┘ │
    │ │ │ │
    │ ▼ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────────────┐ │
    │ │ Runtime Data Areas │ │
    │ │ Metaspace (classes) │ Heap (objects) │ Stack (frames) │ PC Registers │ │
    │ └─────────────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────────────┐ │
    │ │ Garbage Collector (G1 · ZGC · Shenandoah · Parallel) │ │
    │ └─────────────────────────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ │
    │ ┌─────────────────────────────────────────────────────────────────────────────┐ │
    │ │ Native Interface (JNI) · JVM TI · OS Threads · Hardware │ │
    │ └─────────────────────────────────────────────────────────────────────────────┘ │
    └───────────────────────────────────────────────────────────────────────────────────┘
    Production Docker (slim): eclipse-temurin:21-jre-alpine → JVM + runtime libs only
    CI / Developer laptop: eclipse-temurin:21-jdk → full JDK

    Three architecture views — bytecode execution pipeline, classloader hierarchy, and JDK component map:

    Bytecode execution pipeline
    Source .java
    Developer writes code
    javac
    JDK compiler
    Bytecode .class
    Platform-independent
    Class Loader
    Loads into Metaspace
    Verifier
    Safety checks
    Interpreter
    Cold execution
    JIT C1/C2
    Hot native code
    Every method starts interpreted; hot paths compile to native machine code.
    Classloader delegation hierarchy
    Bootstrap
    java.* core classes
    Platform
    JPMS platform modules
    Application
    Your JARs & deps
    Custom
    Spring Boot LaunchedURL
    Parent-first delegation — child asks parent before loading itself.
    JDK component map
    javac
    Compile
    jar / javadoc
    Package & docs
    jshell
    REPL
    jdeps
    Module deps
    jdb
    Debugger
    JDK tools map to distinct engineering workflows — build, debug, profile, ship.
    JIT tiered compilation (HotSpot)
    Interpreted
    Tier 0
    C1 compile
    Tier 1–3 quick
    C2 compile
    Tier 4 if hot
    Native code
    Peak throughput
    Methods graduate through tiers as invocation counters exceed thresholds.

    Code walkthrough

    Trace bytecode from source to execution — compile, disassemble, and inspect what the JVM actually runs:

    • javac (JDK only) — not present in production JRE images by design.
    • javap -c -v — reveals opcodes (lload_0, lcmp) the interpreter/JIT executes.
    • java (JVM launcher) — triggers class loading → verification → execution pipeline.
    java
    // PaymentValidator.java
    public class PaymentValidator {
    public static boolean isValidAmount(long cents) {
    return cents > 0 && cents <= 999_999_99L;
    }
    public static void main(String[] args) {
    System.out.println(isValidAmount(1500L));
    }
    }
    // Step 1 — Compile (JDK tool: javac)
    // javac PaymentValidator.java → PaymentValidator.class
    // Step 2 — Disassemble bytecode (JDK tool: javap)
    // javap -c -v PaymentValidator.class
    /*
    public static boolean isValidAmount(long);
    Code:
    0: lload_0 // load 'cents' param
    1: lconst_0
    2: lcmp // compare cents > 0
    3: ifle 14 // branch if false
    6: lload_0
    7: ldc2_w #2 // max amount constant
    10: lcmp
    11: ifgt 14
    14: iconst_1 // return true
    15: ireturn
    16: iconst_0 // return false
    17: ireturn
    */
    // Step 3 — Execute (JVM loads, verifies, runs)
    // java PaymentValidator

    Production example

    Production Dockerfile + JVM flags — how a fintech payment service separates JDK (CI) from governance JRE (runtime):

    • Multi-stage build — JDK in build stage; only JRE + JAR in production (smaller, safer).
    • -XX:+UseContainerSupport — JVM reads cgroup memory/CPU limits (critical on K8s).
    • ZGC — sub-millisecond pauses for payment latency SLOs; chosen at JVM layer, not application code.
    bash
    # ── CI pipeline (GitHub Actions) — full JDK for build ──
    FROM eclipse-temurin:21-jdk AS build
    WORKDIR /app
    COPY . .
    RUN ./gradlew bootJar -x test
    # ── Production image — JRE only, no javac ──
    FROM eclipse-temurin:21-jre-alpine
    RUN addgroup -S app && adduser -S app -G app
    USER app
    COPY --from=build /app/build/libs/payments.jar /app.jar
    # JVM tuned for container limits + low-latency GC
    ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-XX:+UseZGC", \
    "-XX:+HeapDumpOnOutOfMemoryError", \
    "-XX:HeapDumpPath=/tmp/heap.hprof", \
    "-Xlog:gc*:stdout:time,uptime,level,tags", \
    "-jar", "/app.jar"]
    # ── On-call diagnostics (kubectl exec into pod) ──
    # jcmd 1 VM.flags # verify GC + heap settings
    # jcmd 1 GC.class_stats # class loading stats
    # jcmd 1 Compiler.queue # JIT compilation backlog

    Enterprise case study

    LinkedIn — JVM platform engineering at scale: LinkedIn runs thousands of Java services on Azul Zing/Zulu and Temurin JVMs. Their platform team treats JVM tuning as infrastructure — not per-team artisan craft. When they migrated services to Java 17+, class loading and Metaspace behavior changed with JPMS module boundaries; services using dynamic proxies (Hibernate, Mockito) saw Metaspace growth. Platform engineers standardized on -XX:MaxMetaspaceSize caps, JFR profiling during canaries, and JDK 21 virtual threads for I/O-bound feed services — reducing thread stack memory by 80% on read-heavy paths.

    • Problem: Unbounded Metaspace on services with heavy dynamic class generation.
    • Root cause: Application classloader retained classes from hot-reloaded Groovy scripts + Hibernate proxies.
    • Fix: Metaspace limits, custom classloader lifecycle, JDK 21 virtual threads for I/O concurrency.
    • Lesson: JVM internals are not academic — they drive capacity planning and incident response.

    Performance considerations

    Performance lives in the JVM layer — application code is only half the story:

    • JIT warm-up: First 10k–100k invocations run interpreted/C1 — latency higher until C2 compiles. Use readiness probes; pre-warm with synthetic traffic.
    • Inlining: C2 inlines hot callee methods — micro-benchmarks that don't warm up mislead. Measure at production scale.
    • Escape analysis: JIT allocates objects on stack when proven not to escape — reduces GC pressure without code changes.
    • Compilation queue: Code cache full → JIT stops compiling → performance cliff. Monitor with jcmd Compiler.codecache.
    • Class loading cost: Spring Boot startup loads 15k–25k classes — Metaspace and startup time dominated by classpath scanning, not business logic.

    Security considerations

    JVM security architecture — defense in depth before your code runs:

    • Bytecode verifier: Rejects stack underflow/overflow, illegal type casts, and private field access at load time — prevents entire classes of memory corruption.
    • JDK in production: Shipping javac to prod enables compile-in-place attacks if RCE exists — use JRE-only images.
    • Module system (JPMS): Java 9+ encapsulates internal APIs — sun.misc.Unsafe access requires explicit opens (JDK 21 tightening).
    • JVM supply chain: Pin vendor JDK (Temurin/Corretto) with SHA-verified downloads; CVE patches are JVM-level, not Maven dependency bumps.

    Scalability considerations

    Scaling Java services requires JVM-aware capacity math:

    • Heap sizing: -XX:MaxRAMPercentage=75 on K8s — leave headroom for Metaspace, thread stacks, native memory, and direct buffers.
    • Metaspace per pod: ~80–150MB typical Spring Boot; dynamic class gen services need explicit caps and monitoring.
    • Thread stacks: Platform threads = ~1MB stack each. 500 threads ≈ 500MB before heap. Virtual threads (Java 21) collapse this for I/O workloads.
    • JIT code cache: Default 240MB — large apps with many hot methods may need -XX:ReservedCodeCacheSize increase.

    Production challenges

    Real production failures mapped to JVM subsystems:

    • ClassNotFoundException: Wrong classloader context — common in app servers with multiple WARs or Spring Boot DevTools restart classloader.
    • NoClassDefFoundError: Class was found at compile time but missing at runtime — dependency scope wrong in Maven/Gradle (provided vs compile).
    • Metaspace OOM: Not heap OOM — class metadata exhausted. Hibernate, CGLIB, Groovy, OSGi are frequent culprits.
    • JIT deoptimization: Assumption invalidated (e.g. monomorphic call site becomes megamorphic) — performance drops suddenly. Visible in JFR as "Deoptimization" events.
    • Container OOMKill: JVM heap + off-heap exceeds pod limit — JVM dies without heap dump. Always set MaxRAMPercentage, not fixed -Xmx on K8s.

    Common mistakes

    • Using JDK and JRE interchangeably in Docker — production should never include javac.
    • Assuming Class.forName() uses the same loader as Spring's component scan — it often does not.
    • Setting -Xmx equal to container memory limit — native memory and Metaspace need headroom.
    • Ignoring JIT warm-up in load tests — benchmark after warm-up or results mislead autoscaling decisions.
    • Thinking JRE still exists as a separate Oracle download — since Java 11, use vendor JRE images or jlink.

    Debugging guide

    JVM diagnostics playbook — map symptoms to subsystems:

    • Class loading: jcmd <pid> VM.classloader_stats — which loaders, how many classes.
    • JIT state: jcmd <pid> Compiler.perfmap or JFR "Compilation" events.
    • Metaspace: jcmd <pid> VM.metaspace — used vs committed vs limit.
    • Bytecode inspect: javap -c -private com.example.MyClass on the deployed JAR class.
    • Verbose class loading: -verbose:class at startup (dev only) — trace every loaded class.
    bash
    # Production triage sequence
    java -version # confirm vendor + patch level
    jcmd 1 VM.flags # GC, heap, container flags
    jcmd 1 VM.classloader_stats # classloader leak suspect
    jcmd 1 GC.class_stats | head -30 # loaded class counts
    jcmd 1 VM.metaspace # Metaspace pressure
    jfr start duration=60s filename=/tmp/rec.jfr
    # ... reproduce issue ...
    jfr print --events jdk.Deoptimization,jdk.GarbageCollection /tmp/rec.jfr

    Best practices

    • Use multi-stage Docker builds: JDK for compile, JRE for runtime — standard at Amazon, Netflix, Goldman.
    • Pin vendor JDK (Temurin 21 or Corretto 21) in CI and production with identical patch levels.
    • Set -XX:+UseContainerSupport and MaxRAMPercentage on all K8s-deployed JVMs.
    • Cap Metaspace with -XX:MaxMetaspaceSize on services using dynamic proxies or scripting engines.
    • Enable JFR in production with low overhead — continuous profiling beats post-mortem heap dumps alone.
    • Use jlink for edge/serverless when image size matters — custom runtime with only required modules.

    Anti-patterns

    • Installing full JDK on production servers "just in case" — expands attack surface and image size.
    • Creating custom classloaders without understanding delegation — breaks parent visibility and causes CNFE.
    • Disabling bytecode verification (-Xverify:none) — never in production; removes foundational safety.
    • Running load tests on cold JVMs and declaring SLO failure — warm up first or use JFR to exclude compile phase.
    • Using System.gc() in application code — fights the GC algorithm; tune flags instead.

    Staff engineer notes

    • In architecture review, ask: "What JDK ships in prod, what GC, what heap percentage of pod limit?" — answers reveal JVM maturity.
    • The JRE deprecation is a feature, not a loss — modular jlink runtimes are smaller and more secure than monolithic JRE.
    • Classloader leaks are the #1 cause of Metaspace OOM in long-lived Spring services — understand lifecycle before blaming GC.
    • JIT compilation is why Java wins on throughput for services running weeks — but loses on cold start vs GraalVM native-image. Document workload fit in ADRs.
    • When interviewing senior candidates, "explain classloader delegation" separates people who ran Java from people who operated it.

    Interview questions

    Interview preparation

    15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    5
    1. 1What is the difference between JDK, JRE, and JVM?
      Beginner

      Model answer

      • JVM executes bytecode
      • class loading, verification, interpreter, JIT, GC. JRE was JVM + runtime libraries (deprecated standalone since Java 11). JDK is JRE + development tools: javac, jar, jfr, jlink, jcmd. Developers use JDK; production containers typically ship JRE-only images.

      Follow-up probe

      Why did Oracle remove the standalone JRE?

    2. 2What is bytecode and why not compile directly to machine code?
      Beginner

      Model answer

      • Bytecode is platform-independent intermediate instructions in .class files. Compiling to bytecode enables WORA
      • same JAR runs on Linux, Windows, macOS. JVM JIT compiles hot bytecode to native code at runtime with profile data static compilers lack.

      Follow-up probe

      Name two JVM bytecode instructions.

    3. 3Explain the classloader delegation model.
      Beginner

      Model answer

      • When a classloader receives a load request, it delegates to its parent first. Bootstrap loads java.*, Platform loads platform modules, Application loads classpath JARs. Child sees parent classes but not sibling classes
      • prevents duplicate core class definitions.

      Follow-up probe

      What is a classloader leak?

    4. 4What does the bytecode verifier check?
      Beginner

      Model answer

      • Stack depth consistency, valid type operations, no illegal access to private members, and control flow integrity. Runs at class load time before any bytecode executes
      • foundational JVM security sandbox.

      Follow-up probe

      Can you disable the verifier?

    5. 5What is the difference between JDK tools javac and java?
      Beginner

      Model answer

      • javac compiles .java source to .class bytecode
      • development-time only. java is the JVM launcher that loads classes, starts main(), and manages the runtime. java exists in JRE; javac only in JDK.

      Follow-up probe

      What happens if you run java without a main method?

    Intermediate

    5
    1. 6Explain interpreter vs JIT compilation in HotSpot.
      Intermediate

      Model answer

      • Interpreter executes bytecode directly
      • fast startup, slower steady-state. JIT (C1 then C2) compiles hot methods to native code using runtime profiling. Tiered compilation balances startup (C1) with peak throughput (C2). Cold methods stay interpreted.

      Follow-up probe

      What triggers JIT compilation?

    2. 7What are C1 and C2 compilers?
      Intermediate

      Model answer

      • C1 (client compiler): fast compile, basic optimizations
      • tiers 1-3. C2 (server compiler): aggressive inlining, escape analysis, loop unrolling
      • tier 4. Modern HotSpot uses tiered compilation: methods graduate C1 → C2 as invocation counters exceed thresholds.

      Follow-up probe

      What is deoptimization?

    3. 8Why use jlink instead of a full JRE?
      Intermediate

      Model answer

      • jlink creates custom runtime images with only required modules
      • 40-80MB vs 200MB+ full JRE. Reduces attack surface, container size, and startup classpath scanning. Used in serverless and edge deployments.

      Follow-up probe

      Which module is always required?

    4. 9How does Spring Boot fat JAR class loading work?
      Intermediate

      Model answer

      • LaunchedURLClassLoader loads classes from nested JARs inside BOOT-INF/lib/. Parent is AppClassLoader. Custom loader enables 'run java -jar app.jar' with embedded dependencies
      • but creates classloader complexity for agents and hot reload.

      Follow-up probe

      Why does DevTools restart use a separate classloader?

    5. 10What JVM flags would you set for a Java 21 payment service on Kubernetes?
      Intermediate

      Model answer

      -XX:+UseContainerSupport, -XX:MaxRAMPercentage=75, -XX:+UseZGC, -XX:+HeapDumpOnOutOfMemoryError, -XX:MaxMetaspaceSize=256m, -Xlog:gc* for observability.

      Readiness probe waits for JIT warm-up.

      JFR enabled for continuous profiling.

      Follow-up probe

      Why MaxRAMPercentage instead of -Xmx?

    Advanced

    5
    1. 11Walk through what happens when you run java -jar app.jar.
      Advanced

      Model answer

      MF.

      Application classloader loads main class → verifier checks → main() invoked via reflection.

      run() scans classpath, creates ApplicationContext (loads thousands of classes into Metaspace), embedded Tomcat starts.

      Follow-up probe

      Where is most startup time spent?

    2. 12Design JVM observability for 500 microservices.
      Advanced

      Model answer

      Standardize base flags: UseContainerSupport, MaxRAMPercentage, GC logging, HeapDumpOnOOM.

      g.

      Datadog/async-profiler).

      jcmd runbooks per alert.

      Dashboards: GC pause p99, Metaspace usage, JIT queue depth, class count.

      Canary deploys with JFR diff.

      ADR per GC choice.

      Follow-up probe

      How do you detect classloader leaks?

    3. 13Compare HotSpot JIT vs GraalVM native-image for a Lambda function.
      Advanced

      Model answer

      • Lambda: cold start dominates, short lifetime, moderate traffic. Native-image: AOT compile at build, ~50ms startup, lower RSS
      • wins. HotSpot: 2-5s warm-up, higher peak throughput if function runs long
      • loses on cold start. Hybrid: SnapStart (CRaC) on HotSpot for checkpoint/restore.

      Follow-up probe

      What do you lose with native-image?

    4. 14Explain Metaspace vs Heap — when does each OOM?
      Advanced

      Model answer

      • Heap: object instances (new Foo()). Metaspace: class metadata
      • method bytecode, constant pool, annotations. Metaspace OOM when too many classes loaded (Hibernate proxies, Groovy, OSGi, classloader leak). Heap OOM when objects retained (memory leak). Different diagnostics, different fixes.

      Follow-up probe

      Can GC clean Metaspace?

    5. 15How would you debug a NoClassDefFoundError in production?
      Advanced

      Model answer

      • 1) Read stack trace
      • which class missing. 2) Check if compile-time dep scope is wrong (provided/test). 3) Inspect fat JAR
      • is class in BOOT-INF/lib? 4) Classloader context
      • thread context classloader vs app loader. 5) jcmd VM.classloader_stats. 6) Compare local vs prod JDK version for removed APIs.

      Follow-up probe

      Difference from ClassNotFoundException?

    Hands-on exercise

    Lab: JVM platform discovery — run in the playground, then complete these steps on your local JDK 21:

    • Run the playground code — observe class name, classloader type, and runtime version.
    • Locally: compile with javac Main.java, disassemble with javap -c Main — identify 3 bytecode opcodes.
    • Run with java -verbose:class Main 2>&1 | head -20 — count how many classes load before main executes.
    • Write one paragraph: which Docker base image (JDK vs JRE) you would use for CI vs production and why.

    JavaJDK vs JRE vs JVM

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.

    Architecture trade-offs

    • Full JDK vs JRE in Docker: JDK wins for simplicity in small teams; JRE/jlink wins for security, size, and compliance at scale.
    • Interpreter-first vs AOT (GraalVM): HotSpot JIT wins sustained throughput; native-image wins cold start and memory for short-lived workloads.
    • Default classloader vs custom: Default wins for 99% of apps; custom loaders needed for plugin systems, OSGi, app servers — at cost of complexity and leak risk.
    • G1 vs ZGC: G1 default, balanced; ZGC for strict p99 latency on Java 21 with larger heap headroom for concurrent GC threads.

    Summary

    JDK vs JRE vs JVM is the operating system of Java engineering. You now understand how bytecode flows through class loaders and the verifier, how HotSpot JIT tiered compilation delivers peak throughput, and how enterprises like LinkedIn operationalize JVM tuning at scale. Every future lesson — OOP, collections, concurrency — runs inside this runtime. Next: Java syntax and the type system.

    Key takeaways

    • JDK = develop (javac, jfr, jlink); JRE = run (modular since Java 11); JVM = execute (classload, verify, interpret, JIT, GC).
    • Bytecode is the portability contract — JVM verifies safety, interprets cold paths, JIT-compiles hot paths to native code.
    • Classloader delegation (Bootstrap → Platform → Application) governs visibility — leaks here cause Metaspace OOM.
    • Production ships JRE-only images with container-aware flags; JDK stays in CI — multi-stage Docker is the enterprise standard.
    • Staff engineers debug at the JVM layer: jcmd, JFR, javap, Metaspace caps — not just application stack traces.
    Ready to mark this lesson complete?Track your journey across the entire course.