How the JVM works under the hood, one concept at a time.
Why trading RAM for CPU is usually the economically correct call given how cloud and consumer hardware price RAM per core, how to measure real allocation rate and GC CPU cost instead of guessing, why naive cross-language memory benchmarks mislead, and what adaptive heap sizing is proposing to automate next.
Why the JVM's moving, generational collectors never actually free an object the way malloc/free or reference counting do, and how that design turns GC's CPU cost into a direct function of heap headroom you control with -Xmx.
How conditional branches always compile to the inverse of the source condition, why == on objects is if_acmpeq (identity, never equals()), why a switch compiles to tableswitch or lookupswitch depending on label density, why return is six type-specific instructions instead of one, and why a synchronized block costs a compiler-generated exception table that a synchronized method never needs.
Why 'new' is allocate-then-construct as two separate instructions, how field and array access split into distinct opcode families, and why the JVM has five different invoke* instructions instead of one — including why modern javac no longer emits invokespecial for private method calls.
How bytecode's stack-machine arithmetic works — the i/l/f/d mnemonic prefixes, why float and double comparisons need two opcodes to handle NaN correctly, and why narrowing conversions like d2i and i2b lose data silently instead of throwing.
How ZGC's colored pointers and load barrier keep pauses sub-millisecond by moving objects concurrently, what generational ZGC changed in JDK 21-24, and why an allocation stall — not a pause — is ZGC's actual failure mode to monitor.
How G1 lays out the heap as regions instead of fixed generations, why a 1MB+ allocation can skip eden and land straight in old, and how to read a GC log line well enough to tell a healthy young collection from an evacuation failure.
Why collections are unsynchronized by default, why sizing a collection up front avoids real resize costs, and why a chain of Stream operations processes far less data than it looks like it does.
Why Java servers moved from one-thread-per-connection to NIO selectors and async response patterns, and how virtual threads have since made most of that ceremony unnecessary to write by hand.
How the JVM's total footprint (heap plus native memory) works, why reserved memory isn't the same as memory actually in use, and how to break it down with Native Memory Tracking.
Why prepared statements only pay off when pooled per connection, and why JPA's eager relationship fetching issues one query per related entity instead of a single JOIN — the classic N+1 query problem.
How compact strings roughly halve the heap cost of an average String, when G1 string deduplication is worth turning on, and why the compiler's own concatenation optimization usually beats a hand-rolled StringBuilder chain.
Why a hand-rolled "time a loop" benchmark routinely measures nothing (dead-code elimination, missing warm-up, constant folding), and how JMH's Blackhole exists specifically to close those holes.
How JFR's event-based, sub-1%-overhead recording lets you profile a live production JVM continuously, and how to control it entirely from the command line.
Why sizing a ThreadPoolExecutor for compute-bound work means matching the core count, sizing for I/O-bound work means far exceeding it, and which half of this problem virtual threads actually solved.
How to use heap histograms and heap dumps to find out what's consuming memory, and how to read the different flavors of OutOfMemoryError instead of just adding more heap.
How the JVM's generational collectors trade off pause time, throughput, and CPU overhead, and which one the JVM actually defaults to today versus what a 2020-era book assumes.
How the JVM compiles bytecode to native code while the program runs, why it waits for code to get "hot" before compiling it, and how tiered compilation combines a fast-starting compiler (C1) with a slower, smarter one (C2).
How javac turns Java source into a structured binary .class file — magic number and version in the header, the constant pool's table of symbolic references, access flags, how fields and methods are laid out for the JVM to load and execute, and the one-letter descriptor codes (B/C/D/F/I/J/S/Z, L...;, [) that encode every type in the file.