A complete visual teardown of the JVM, taught from zero, where you run a real JDK 27 from a notebook cell and the notebook is running on the machine you are studying.
Pinned to jdk-27+35, the release candidate, moving to jdk-27-ga when that tag lands on 15 September 2026.
There are five kinds of JVM material in the world and none of them get you from "I have typed -Xmx4g a thousand times" to "I can read a deoptimization trace, write a JVMTI agent, and open a pull request against openjdk/jdk".
The Java Virtual Machine Specification is a real normative document and it is excellent. Chapter 4 defines the class file byte for byte, Chapter 5 defines loading and linking as a state machine, Chapter 6 defines all 202 opcodes. It also describes an abstract machine that does not exist. There is no JIT in it, no garbage collector, no object header, no safepoint, no deoptimization, so a reader who masters it still cannot explain why their benchmark got faster after ten thousand iterations.
The books are good and they are behind. The only book that ever really taught HotSpot's execution engine has a second edition from 1999, which is before C2, before tiered compilation, and before every collector currently shipping. Shipilev's JVM Anatomy Quarks are the best writing about the JVM that exists, and they are deliberately short, deliberately non linear, mostly undated, and they stop where the interesting part starts. Conference talks and blog posts are enormous in volume, undated by convention, and overwhelmingly about G1 tuning flags. And then there is the source: about 1.7 million lines of C++ in src/hotspot, two compilers, five collectors, an interpreter that is generated at runtime as machine code, and tens of thousands of jtreg tests.
This project is the missing path. Every claim points at a line of HotSpot with a version tag on it, every behaviour is something you watch happen rather than something you are told, and every claim is marked as either something the specification requires or something HotSpot chose.
A real JDK 27, in a notebook, in about thirty seconds, for free, and the notebook is running on the JVM you are studying. Every lesson is a Jupyter notebook with an Open in Colab badge. Colab is an Ubuntu VM with root, not a sandbox, so you run the actual java, the actual javac, the actual jcmd. The kernel is JJava, a JShell backed Java kernel, so your REPL is a live HotSpot instance and ManagementFactory, JOL, Thread.ofVirtual and the whole jdk.internal surface apply to the very VM executing the cell. When a lesson says "watch this method get compiled", the method being compiled is the one you typed.
Modifying the runtime is a beginner activity, and there are three supported ways to do it without building a JVM. A java.lang.instrument agent is one Java file and about two seconds of javac, and it rewrites bytecode at class load time inside the real VM. -XX:CompileCommand lets you forbid an inline, force a compilation or print the assembly for one method without touching a compiler. A JVMTI agent is a shared library against a stable documented interface with hundreds of functions and events. All three are supported API rather than a private test module.
Building OpenJDK fits inside a Colab session. Roughly twenty five minutes with a boot JDK and no cache. That is the difference between "patch the template interpreter and watch your opcode run" being a lesson and being a wish, and it is the thing that the LLVM project in this series could not offer.
The Class File API is standard and in the JDK. JEP 484 finalised java.lang.classfile in JDK 24, so every bytecode lesson gets a machine checkable constructor as well as a reader. You do not merely disassemble javac output, you build a class file by hand in a cell, load it, run it, break it, and watch the verifier explain which rule you violated. No ASM, no ByteBuddy, no third party version skew.
HotSpot ships a machine readable description of its own memory layout, and the tool that reads it. vmStructs.cpp names every VM struct, every field, its type and its offset, exported so an external process can walk a live JVM or a core file. So the data structures section of every Blueprint is generated rather than transcribed. Alongside it, globals.hpp generates the flag reference, bytecodes.cpp generates the opcode tables, jvmti.xml generates the JVMTI reference, JFR metadata generates the event reference, and the .ad files generate the C2 instruction selection reference per architecture.
There is a real specification, it is not ours, and separating it from the implementation is the whole point. Every claim in every lesson and every clause in every Blueprint carries a marker: [JVMS] with a section reference, or [HOTSPOT] with a source citation. That distinction is the difference between "Java guarantees this" and "this happens to work on my laptop", and it is exactly the distinction that talks and blog posts never make.
There is a machine checkable oracle for concurrency and it is OpenJDK's own. jcstress runs a two actor test billions of times and reports the observed outcomes against the outcomes the Java Memory Model permits, classifying each as acceptable, interesting or forbidden. It makes the memory model observable rather than merely assertable, it grades the boss fights for the whole concurrency part, and a forbidden outcome is a bug in the JVM or in your understanding.
112 lessons in twelve parts, three passes over the whole runtime at increasing depth. Part 0 is orientation, including the lesson that teaches you to tell a guarantee from an implementation detail before anything else happens. Parts I through V take you from a class file in a hex editor to an opcode you added to the interpreter yourself. Parts VI through VIII are the engine room: the JIT, garbage collection, threads and the memory model. Parts IX and X are serviceability and the native boundary, and Part XI is changing the runtime and opening a pull request.
Alongside the lessons there is a Blueprint set, which is the normative half. A Blueprint is a specification with nine fixed sections that may not reference the chapter it accompanies, so somebody can implement from it cold. There are 58 of them. Section 4 is split three ways into what the JVMS mandates, what HotSpot chose, and the ordering obligations, because if the Blueprint blurs that line the project has failed at the thing it exists to do.
Then three capstones. Track A is a class file interpreter in Rust that runs real, unmodified java.base code. Track B is a garbage collector built inside HotSpot on the real CollectedHeap and BarrierSet interfaces. Track C is a baseline JIT with working deoptimization. Their purpose is to test the Blueprints rather than to produce a runtime, and every defect a track finds is filed against the Blueprint and fixed there.
| Environment | What you get | |
|---|---|---|
| E0 | Colab, one badge click | A real JDK 27 with a JShell kernel, plus instrumentation agents, JVMTI agents, jcmd, JFR, and an OpenJDK build in one long cell |
| E1 | Devcontainer or your own machine | The above, plus a full build tree, gdb, hsdis, -XX:+PrintAssembly and honest benchmarks |
| E2 | Browser, no account at all | Pre executed pages with every output visible, plus the recorded transcript corpus |
Every page on the site is complete on first paint. The output you read was produced by a real run of the pinned JDK in CI, so there is no runtime to start and no account needed to read anything. The badge is how you go from reading to running.
E2 is the honest weak spot and this project will not dress it up. There is no Pyodide for Java. CheerpJ targets an old Java level and cannot be pinned, TeaVM is not a JVM, so a reader with no Google account reads results rather than producing them, and the page tells them so.
This project will never claim TCK conformance and says so on the first page of the conformance chapter rather than in a footnote. The Java Compatibility Kit is the only thing that can call something a Java Virtual Machine in the certification sense, and its licence covers implementations derived from OpenJDK and OpenJDK participants. It is not available to an independent implementation, so no artifact here will ever say "passes the TCK".
What replaces it is open, in tree and adversarial: jtreg, jcstress, JMH, differential execution against HotSpot, OpenJ9, GraalVM and ART, and class file fuzzing. The scorecard publishes a number with its failures classified, and the disclaimer sits on the same page as the numbers.
Specification stage. Nothing is built yet.
The current milestone is M0, which exists to measure the assumptions this design rests on, and it is the one milestone allowed to fail. Can a free Colab runtime become a pinned JDK 27 kernel in under 90 seconds. How much do JShell's synthetic per snippet classes distort a class histogram, a compilation log or a JFR recording. Can bpc pull struct layouts out of the Serviceability Agent's type database. Do the widgets survive Colab's output sandbox. If the answers come back wrong, M0 ends with a written re plan rather than with M1, and finding that out in week one is the point.
The second of those is answered. How much does JShell distort what a lesson can observe measures the same workload four ways and draws the line: anything about one object you are holding is safe in the notebook kernel, anything that counts, totals or names classes has to run in a subprocess.
So is the question of whether the curriculum has to fork by platform. What a machine can actually do asks a JDK and the box under it 119 questions on macOS, on Linux as root, on Linux as an ordinary user and on Windows, and 103 of the answers are the same on all four. Every one of the sixteen that differ is machine size, privilege, timing or a native tool somebody installed. None of them is a difference in what the JVM can do.
The verification lessons have their raw material. Six class files that are wrong on purpose tries to build each malformation B11 needs and records where the JVM notices, and four of the six come straight out of java.lang.classfile while the other two need about five lines of byte patching. The one with a constant pool index past the end of the pool is worth the trip on its own: with the verifier turned off it kills the VM ten times out of ten on Linux, three times out of ten on Windows and once out of ten on a Mac, which is what undefined behaviour looks like when you measure it instead of describing it.
The widget question came back with an answer to a different question. What a Java kernel can put on the screen tries twelve ways of getting something interactive in front of a reader and checks each one in four places, and the thing that decides the answer turns out not to be Colab's sandbox at all. It is Jupyter's trust model: a saved notebook nobody has run gets its style tags removed, its ids renamed, its form controls disabled and its scripts dropped, so only four of the twelve survive the state most readers will meet the page in. That is a smaller budget than the design assumed and it is enough, because <details> and an SVG in an <img> are both on the list that always works.
The generated half of every blueprint has a source. Reading HotSpot's own struct layouts tries four ways into the Serviceability Agent's type database on three machines, and an ordinary user on Linux gets in three of them, because ptrace_scope at 1 permits attaching to a process you started yourself. Every door that opened read the same numbers, and they match what jhsdb clhsdb prints by hand. The database also holds less than the design assumed: markWord has a size and not one exported field, so the bit layout that half the object layout lessons depend on has to keep coming from the header file.
The JIT lessons can show instructions. A disassembler for the JIT lessons builds hsdis from the pinned JDK source three ways, and all three work, load from a directory on LD_LIBRARY_PATH without touching the JDK, and print the same 293 instructions in three syntaxes. Building one takes about a minute, so the choice between them is a licence question rather than a technical one, and the generated page prints what each backend's packages declare rather than a conclusion. The fallback the issue hoped for is not one: -XX:+PrintOptoAssembly on a product build prints 330 headings and no bodies.
And the first generated blueprint section exists. Emitting a section 2 that nobody transcribed turns those three measurements into the object header's data structures section for markWord, oopDesc, Klass and InstanceKlass. It takes three sources because no one of them has the whole layout, it states the flags the layout is contingent on before it states the layout, and it refuses to write anything if the mark word does not tile 64 bits, if a field lies outside the struct that declares it or if a subclass starts inside its superclass.
And every line this repository points at is checked. tools/refcheck.py resolves all thirty four source citations against the OpenJDK tree at the tag each one names, and hashes the cited line with two lines either side into a committed ledger. A line number that drifts is obvious to a reader, and a line number that still exists and now says something different is not, so the hash is the part that matters. The first run of it found a citation in the contributing guide pointing at a row of dashes, which is the kind of thing a checker exists to find. The specification citations resolve too, against an index of all 177 sections of JVMS SE25 built from Oracle's published edition, and the report prints the title each section actually has beside the claim that cites it, because a citation that resolves is not the same as a citation that is right.
And the tool the object lessons hand the reader has been checked against the VM rather than trusted. Does JOL tell the truth about compact object headers runs JOL 0.17 and the VM's own -XX:+PrintFieldLayout inside one JVM so the two halves cannot be describing two different VMs, across twelve classes, four flag configurations and both attach modes. JOL is right: every offset it reports is the offset the VM printed, including in the configuration where JOL prints a warning saying its answers are not reliable, because that warning is about addresses and not about offsets. The one thing it misses is a field that does not exist in Java, a flags byte HotSpot injects into java.lang.String where reflection cannot see it. Getting that answer needed a fastdebug JDK built from the pinned commit, because PrintFieldLayout is a develop flag that is compiled out of every build a reader can download.
And a resolving citation is not yet an honest one, so tools/claimcheck.py holds each lesson and its ledger of claims to each other in both directions: no claim whose marker appears in no sentence, no marker with no claim behind it, and nothing calling itself observable that records no measurement. Running it on the first lesson found four things wrong, and all four were real. Three were claims the ledger recorded that no sentence in the lesson actually marked, and two of those three were pointing at a comment describing the layout when the honest citation is the flag default in globals.hpp and the rounding in align_object_size. The fourth said it was observable and had measured nothing, and fixing it needed a measurement rather than an argument, because the lesson and the helper it ships with disagreed about whether a semicolon saves you from JShell writing an identity hash into an object you were about to look at. It does not.
Five probes in this milestone cannot be run from here at all, and the answer to that is neither a guess nor an open issue with nothing behind it. Four of them are about Google Colab, which cannot be asked a question except through a browser signed in to a Google account, and the fifth needs hardware that is doing nothing else. So each one is written down as something a person can run in one sitting: a notebook or a script that ends by printing a JSON block, and a protocol saying what to click and what to paste back. The notebooks are built from readable sources and checked in CI, so the thing somebody opens cannot drift from the thing a pull request reviewed. The jcstress one carries its own five tests, three with an interesting outcome and two controls whose forbidden outcome must never fire, because jcstress does not publish its test corpus to Maven Central and a run of all zeroes has to be distinguishable from a run that did nothing.
M2 has started at the piece of it that has to be generated. Three answers to how many opcodes there are builds the instruction set section by joining the specification's opcode list, HotSpot's bytecodes.cpp and java.lang.classfile.Opcode read off the pinned JDK, and the three give three different counts because they are answering three different questions. It also counts, because a table of 205 opcodes read top to bottom suggests they matter about equally: over 3,902,996 instructions in java.base on two platforms, thirteen of them are half of all bytecode, fifty two are ninety per cent, and nineteen never occur at all. The finding a lesson author needs is in the operands column, where nine instructions including every invoke carry an operand that HotSpot rewrites into native byte order when it links the method, which is why a bytecode dump out of a running VM and javap on the same class disagree and neither is wrong.
The other half of that exit criterion is the format itself, and it is generated the same way. What is actually in fourteen thousand class files builds the class file structure section from JVMS chapter 4, the header the JDK ships to native agent authors, and java.lang.reflect.AccessFlag and java.lang.classfile.Attributes read off the pinned JDK, and it counts every class file in java.base on two platforms. Seventy per cent of all 2,139,699 constant pool entries are strings and the names of things. Eighteen of the twenty three access flags mean different things at different class file versions, which is the fact a printed flag table cannot carry and the reason a parser written from one is right about today and wrong about a file from 2015. And the census found two things nobody wrote down: one class file in java.base is at version 52 because a literal in jlink says so while the same file asks the platform for the version seven hundred lines earlier, and four classes set a bit in access_flags that the specification reserves and says should be zero.
The third generated piece of M2 is the one where the source cannot answer on its own. Fifty three flags say they are at their default and are not builds BP-FLAGS by joining the 22 HotSpot globals headers at the pinned tag, which declare 1,262 flags, against a JVM run a dozen times and asked each time what flags it has, on macOS arm64 and inside a Linux container. A header knows a flag's type, default and range. Only a running VM knows its origin, and the origin is where the join earns its keep: 53 flags report {default} while holding a value no header declares, because FLAG_SET_DEFAULT is an assignment to a global variable with nowhere in it for an origin to be recorded, and the file that detects your processor uses it 80 times and its origin stamping sibling not once. The same join finds that unlocking a diagnostic flag changes what you may set and never what you may see, that seven ways of being refused produce seven differently worded sentences, and that this VM will list UseShenandoahGC as a product flag with a default and then refuse to start with it on.
The fourth is the one that checks the specification rather than quoting it. Two rules the specification protects with a VerifyError, and HotSpot with something else breaks one valid class file 27 named ways and 256 random ways, and compares each rejection against 23 sentences quoted out of JVMS SE25 and checked against the chapter on every run. Two cases answer M2's fuzzer gate: a max_stack too small and a max_locals too small are both constraints from chapter 4.9, which 5.4.1 says throw a VerifyError, and both arrive as a ClassFormatError. The max_stack one says StackMapTable format error: bad type array size, which names no rule that exists, and the reason is in a comment at the call site: the stack map reader validates frames against max_stack while parsing them and reports through an error class chosen so that an old class file can fail over to the type inferring verifier, a failover this class file's version makes impossible. Twelve of the 27 messages use none of the words their own rule uses, including all four access flag pairs, which report the sum of the flags in hex and name neither of them. And 40 of the 256 single bit flips load and link with nothing said at all.
Those four generated pages now have a document to be sections of. tools/bpc.py compiles a blueprint from a directory of nine numbered section files into one page and one machine readable checklist, and the first two are BP-CLASSFILE and BP-BYTECODE, whose section 2 and section 3 are the generated pages above rather than anything a person retyped. It is a compiler rather than a convention because of one rule. A clause in 4a states what the specification mandates and cites the specification, a clause in 4b states what HotSpot chose and cites a line of its source at the pinned tag, and a clause in the wrong half fails the build, because a sentence that reads as "Java guarantees this" and is really a fact about one implementation at one tag is the single defect that would make this project worse than a blog post. It refuses four more things that are each a defect somebody would otherwise ship: a clause no section 5 item says how to watch and with what tool, a clause no section 8.2 item tests, an observation with no tool in it, and a hand written table that retypes a row a generated section already has. The two blueprints carry 66 clauses and 36 conformance items, every clause observed and tested, and writing them left one item, 8.2.11, that the pinned build fails, which is a finding about HotSpot that stays in the checklist rather than a defect in it.
And the format now has somewhere to be typed rather than read. The class file playground is a page where you write the ten top level fields of a class file in the order chapter 4 lists them, one call per field, and end up with 299 bytes that javap disassembles into the four instructions you wrote, that the VM running the notebook loads and links, and that a fresh JVM runs and prints from. The helper behind it does the three things that carry no teaching and nothing else: it hands out constant pool indices, it fills in the length fields that cannot be written until the bytes they count exist, and it remembers what every byte was for, so the file prints as a hex dump with its own field names beside it. Not one number in it is typed. The magic number, the class file version, every constant pool tag and every opcode are read off your JDK through java.lang.classfile at the moment you ask for them, which is also why the page cannot quietly go stale against a JDK that adds one. Then it breaks the file five ways, and the last two are the finding: a max_stack of zero in a method with no branches is refused by the verifier with a message that names the stack, which is what the specification says should happen, while the same mistake in a method with a branch comes back from the parser as StackMapTable format error: bad type array size, because a branch means a StackMapTable and HotSpot checks the declared stack depth while reading that attribute rather than while verifying. Same rule, same file, different error class, decided by whether the method happens to have a branch in it. A playground is a new kind of page rather than a lesson, with no prediction gate and no grader, because there is nothing here to be right about, and CI takes the code cells out of the committed notebook and runs them on the pinned JDK, since a page whose whole promise is that it runs is the one kind of page that reading the diff cannot check.
The third blueprint is the one where the specification turns out to be enforced in a place nobody would look. Two million constant pool entries, and what all the strings are builds BP-CONSTPOOL section 2 by joining JVMS chapter 4, the tag header the JDK ships to native agent authors, HotSpot's own constantTag.hpp and constantTag.cpp, and java.lang.classfile read off the pinned JDK, and then walks every class file in java.base. Whether ldc may push an entry depends on the class file version, the specification gives it a table of its own, and exactly one place in the JDK enforces it: the inference verifier written in C, which is reached only for class files below version 50. The type checking verifier that everything modern goes through builds a fixed mask of seven tags with no version in it. The arrangement is correct, and it is correct because the lowest threshold in that table is 49 and the verifier switchover is at 50, two numbers chosen twenty years apart, and nothing in the code would notice if either moved. The census half answers a different question the format cannot: 56% of all entries are strings, nothing in a string says what it is for, and asking what points at each one accounts for all 1,216,114 of them under thirty roles, the last two of which are attributes no specification defines. Local variable names and their descriptors are 758,196 uses, over a quarter of all string use in the module and every byte of it debug information. BP-CONSTPOOL carries 39 clauses, 20 observations and 19 conformance items on top of that, and its one unobservable item is the three clauses about the tag byte's memory ordering, which need a jcstress run on weakly ordered hardware that has not happened yet.
The fourth blueprint is the one where the measurement found a property nothing promises. Two hundred thousand stack map frames, and not one of them a byte too long builds BP-STACKMAP section 2 by joining JVMS 4.7.4, the tag header the JDK ships to native agent authors, HotSpot's stackMapTable.hpp and verificationType.hpp, and java.lang.classfile read off the pinned JDK, and then decodes every frame in java.base twice, once through the class file API and once as raw bytes. Six of the seven frame kinds are a compression of the seventh, the specification has no canonical form and no shortest form rule, a file using full_frame everywhere is legal and loads, and every one of the 205,580 frames in the module is written in the smallest form the encoding allows. Nothing requires that of anybody, and it means two compilers that agree about the types at every branch target emit byte identical attributes, so a difference in these bytes is a difference in what was proved rather than in how it was written down. The probe stops looking and starts building where looking cannot work: three of the nine verification types never occur on one of the two sides the format permits, so it assembles a class that puts them there, hands the 316 bytes to the running JVM, and refuses to write a page unless the JVM's own verifier accepts it. Reading the decoder found three more things: it consumes the two byte offset delta before it decides the frame type was reserved, its second reserved frame type error is unreachable because the branches above it partition a u1, and at class file version 50 exactly a stack map that fails every check is survivable, because the class fails over to a verifier that does not read the attribute at all. BP-STACKMAP carries 42 clauses, 21 observations and 21 conformance items, every clause observed and tested, and its one unobservable item is an aliasing decision inside the reader that no outside instrument can distinguish from the alternative.
The fifth blueprint is the one where the measurement contradicts what everybody says the verifier does. The verifier cannot tell java.lang.Object apart from an interface builds BP-VERIFY section 2 by joining JVMS 4.10.1.2, HotSpot's verificationType.cpp, the two version constants in verifier.hpp and verifier.cpp, and a grid of 256 class files. The specification gives the assignability relation as 22 Prolog clauses over fourteen type terms, which is small enough to paraphrase, and paraphrasing it is how the finding stays invisible. So the probe does not read the clauses. It builds one class file per ordered pair of sixteen verification types, each producing a value of the row's type into a local that the stack map frame declares as the column's type, and asks the pinned JVM. 65 of the 256 load, 191 come back as VerifyError in one sentence shape, and two platforms agree on every cell. The relation is reflexive, it is not transitive, and it is not antisymmetric: six pairs are assignable both ways and they close into one group of four holding java.lang.Object, Runnable, Cloneable and Serializable. So a class file that puts a java.lang.String in a local its frame declares as Runnable, and then calls run on it, verifies, and throws IncompatibleClassChangeError when it runs. Change Runnable to Thread and the same file is refused, because the target happens to carry ACC_INTERFACE. HotSpot says it outright in a comment four lines above the return that does it, the specification says it in a rule head with an unbound argument, and one of its 22 clauses does not parse as Prolog, because there is no comma between a parenthesised disjunction and the goal after it. The same run also settles the open item BP-STACKMAP left behind, writing one wrong frame at five class file versions with a correct frame control at each: at 49 and 50 the class loads anyway. BP-VERIFY carries 46 clauses, 21 observations and 21 conformance items, every clause observed and tested, and nine of its observations have been performed.
See the milestone issues for the plan and ROADMAP.md for the short version.
Prose, diagrams and animations are CC BY 4.0. Code is Apache-2.0. Anything derived from OpenJDK source is GPLv2 with the Classpath Exception and lives in patches/ and vendor/, isolated and badged, and nowhere else. The Classpath Exception is why ordinary lesson code that merely uses the JDK carries no obligation at all. See LICENSE.md.
This project asks you to run things: a bootstrap cell, a kernel from Maven Central, a JVMTI agent as a shared library, a container image, and eventually a JDK you built yourself. SECURITY.md says what is pinned, what is verified, what is published, and how to report it when one of those turns out to be a way to run code a reader did not ask for.
Part of a series that shares its pedagogy, its Blueprint format and its conformance machinery.
- cpython-internals, CPython 3.15
- gcc-internals, GCC 16
- linux-kernel-internals, Linux 7.2
- llvm-internals, LLVM 23