JVM Concepts
How the JVM works under the hood, one concept at a time.
Compilation & Bytecode
The .class File Format: Header, Constant Pool, Fields, and Methods
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.
JIT Compilation and Tiered Compilation: C1, C2, and Warm-Up
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).
Bytecode Instructions: The Operand Stack, Arithmetic, and Type Conversions
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.
Bytecode Instructions: Object Creation, Field Access, and Method Dispatch
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.
Bytecode Instructions: Branching, Returns, and Synchronization
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.
Memory & Performance
Heap Analysis and Reading OutOfMemoryError Correctly
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.
String Performance: Compact Strings, Deduplication, and Concatenation
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.
Native Memory and JVM Footprint: Reserved vs. Committed
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.
Sizing the JVM Heap: RAM/CPU Economics in Practice
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.
Garbage Collection
Choosing a Garbage Collector: Serial, Parallel, G1, ZGC, and Shenandoah
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.
G1 Internals: Regions, Humongous Objects, and Evacuation Failure
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.
ZGC Concurrent Collection: Colored Pointers and Allocation Stalls
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.
Moving Garbage Collectors and the RAM/CPU Trade-off
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.
Concurrency & I/O
Thread Pool Sizing: CPU-Bound vs. I/O-Bound Workloads
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.
NIO Servers, Async Responses, and Virtual Threads
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.
Collections API and Streams: Sizing, Synchronization, and Laziness
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.
Profiling & Benchmarking
Java Flight Recorder: Low-Overhead Production Profiling
How JFR's event-based, sub-1%-overhead recording lets you profile a live production JVM continuously, control it from the command line, read it with jfr view, add your own events, and use the JDK 25 CPU-time and method-timing profilers.
Microbenchmarking Pitfalls and JMH
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.