Production Runtime Case Study: JVM and Java Bytecode
Java programs are compiled by javac into .class files containing JVM bytecode — instructions for a stack machine — plus a table of constants and names.
Learning Objectives
Follow Java source through javac to class files; describe class-file structure, the constant pool and method metadata; read bytecode instructions, the operand stack, local-variable array and frames.
Explain verification, class loading/linking/initialisation, class loaders and delegation, the JVM specification versus implementations, interpreters and JITs, GC, exceptions and exception tables, reflection, JNI, JARs and class/module paths.
Use javap -c; read debug metadata and line tables; recognise common JVM errors; use heap dumps, GC logs and profilers; handle flags portably; reason about class initialisation order, the Java memory model, warm-up and class-loading security.
Plain-Language Explanation
Java programs are compiled by javac into .class files containing JVM bytecode — instructions for a stack machine — plus a table of constants and names. The JVM loads those files when classes are first needed, checks them, runs them in an interpreter, and compiles the busy methods to machine code.
Precise Technical Explanation
Java source and compiler output javac produces one class file per class (including nested and anonymous classes); the JVM runs class files, not Java source (single-file source launch compiles in memory first).
A binary format: magic 0xCAFEBABE, minor/major version (Java 21 = 65), constant pool, access flags, this/super class, interfaces, fields, methods, attributes.
Class-file structure overview Methods carry a Code attribute (max stack, max locals, bytecode, exception table, attributes such as LineNumberTable, LocalVariableTable, StackMapTable).
Numbered entries: UTF-8 strings, class, field and method references, name-and-type pairs, numeric constants, string literals, method handles, invokedynamic bootstrap entries. Instructions reference them by index (#7 in javap).
Name, descriptor ((II)I = takes two ints, returns int), access flags, exceptions, annotations, generic signatures.
About 200 typed instructions: loads/stores (iload_0, astore_1), arithmetic (iadd), constants (iconst_m1, bipush), object operations (new, getfield), invocation (invokestatic, invokevirtual, invokeinterface, invokespecial, invokedynamic), control flow and athrow.
Operand stack and local-variable array Each frame has a local-variable array (parameters first; this in slot 0 for instance methods) and an operand stack with a maximum depth declared in the class file.
Created per method invocation; the verifier uses StackMapTable frames to check types at branch targets.
Type-checking verification proves every instruction gets operands of the right types, stack depths match, and no instruction reads uninitialised locals — before execution.
Class loading, linking, and initialization Loading finds the bytes; linking verifies, prepares static fields, and resolves symbolic references (lazily); initialisation runs <clinit> on first active use (Module 15).
Class loaders and delegation overview Loaders delegate to their parent first (bootstrap → platform → application); a class's identity is its name *plus* its defining loader, which enables isolation in application servers and plugin systems.
JVM specification versus a JVM implementation The JVM Specification defines class files, instruction semantics and verification; HotSpot, OpenJ9 and GraalVM implement it with different interpreters, JITs and collectors.
Interpreters and JIT compilers HotSpot's template interpreter, C1 and C2 (tiered), optionally Graal as the JIT.
Serial, Parallel, G1 (default on servers), ZGC and Shenandoah (low latency), selected with flags.
Exceptions and exception tables Per-method tables of (from, to, target, type) — Module 13 shows a real one.
Class, Method, Field objects; MethodHandles for fast dynamic invocation; reflection respects module encapsulation in JPMS.
Native methods and JNI overview native methods implemented in C/C++ via JNI (or the newer Foreign Function & Memory API): outside the JVM's safety checks.
JAR files and class paths/module paths JARs are ZIP files of class files and resources; the class path is a flat search list; the module path (JPMS, Java 9+) adds explicit dependencies and encapsulation.
javap -c and bytecode inspection javap -c disassembles methods; -v adds the constant pool and attributes; -p shows private members.
Debug metadata and line tables LineNumberTable maps bytecode offsets to source lines; LocalVariableTable (with javac -g) names locals for debuggers.
Common JVM runtime errors NullPointerException (with helpful messages since JDK 14), ClassNotFoundException vs NoClassDefFoundError, UnsupportedClassVersionError, OutOfMemoryError, StackOverflowError, ClassCastException, NoSuchMethodError (binary incompatibility).
Heap dumps, GC logs, and profiling jcmd <pid> GC.heap_dump, -Xlog:gc*, JDK Flight Recorder (-XX:StartFlightRecording), async-profiler, VisualVM.
Runtime flags and portability cautions Standard options (-cp, -Xmx) versus -XX: options, which are implementation-specific and can change or disappear between releases.
Class initialization ordering JLS §12.4: superclass first, then the class's static initialisers in textual order, triggered on first active use; cycles can observe default values.
Java memory model introduction Defines which writes a read may observe across threads (happens-before via synchronized, volatile, thread start/join) — covered in depth in the Concurrency course.
JVM performance and warm-up Interpretation and C1 first; C2 after warm-up; class-data sharing reduces start-up; JIT logs explain slow first requests.
Security and class-loading considerations Loading untrusted class files executes untrusted code (the Security Manager is deprecated for removal — JEP 411 — so isolation needs processes or containers); deserialisation of untrusted data is a classic risk; keep class paths free of writable directories.
Java example (Temurin JDK 21):
Suggested exploration:
javap output depends on compiler version and options; read it semantically rather than expecting an exact listing.
public class HelloRuntime {
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(20, 22));
}
}javac HelloRuntime.java
java HelloRuntime
javap -c HelloRuntimeWhy It Matters in Real Systems
Reading bytecode explains binary incompatibilities, lambda and string-concatenation costs, exception-handling behaviour and what profilers and agents (instrumentation, coverage) actually change.
Key Terms
JVM binary unit: constant pool, fields, methods, attributes Encoded method type, e.g. (II)I
Call site linked at run time by a bootstrap method Finds and defines classes; delegates to parent Type states used by the verifier
Diagram, Execution Trace or State Transition
inspectorSyntax or Instruction Format
javap -c line format: offset: opcode operands // resolved constant. Typed prefixes: i int, l long, f float, d double, a reference.
Complete Example
javap -c of HelloRuntime (Temurin JDK 21 javac; the listing depends on the compiler version):
Reading a class file's header and constant-pool size directly (CPython 3.13.1 parsing the bytes javac from Temurin JDK 21 produced):
Verification in action: a class file whose bytecode has been altered is rejected before running. The program compiles a class, patches its iadd (0x60) into fadd (0x62) — adding floats to ints is a type error — and loads it (CPython 3.13.1 driving Temurin JDK 21):
public class HelloRuntime {
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(20, 22));
}
}Compiled from "HelloRuntime.java"
public class HelloRuntime {
public HelloRuntime();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 20
5: bipush 22
7: invokestatic #13 // Method add:(II)I
10: invokevirtual #19 // Method java/io/PrintStream.println:(I)V
13: return
}import os, struct, subprocess, tempfile
JDK = r"D:\EngineerHub-tools\jdk21\bin"
d = tempfile.mkdtemp()
open(os.path.join(d, "HelloRuntime.java"), "w").write(
"public class HelloRuntime { public static int add(int a, int b) { return a + b; }"
" public static void main(String[] args) { System.out.println(add(20, 22)); } }")
subprocess.run([os.path.join(JDK, "javac.exe"), "HelloRuntime.java"], cwd=d, check=True)
data = open(os.path.join(d, "HelloRuntime.class"), "rb").read()
magic, minor, major, cp_count = struct.unpack(">IHHH", data[:10])
print(f"magic 0x{magic:X}, version {major}.{minor} (Java {major - 44}), constant pool entries {cp_count - 1}, size {len(data)} bytes")magic 0xCAFEBABE, version 65.0 (Java 21), constant pool entries 30, size 462 bytesimport os, subprocess, tempfile
JDK = r"D:\EngineerHub-tools\jdk21\bin"
d = tempfile.mkdtemp()
open(os.path.join(d, "Calc.java"), "w").write(
"public class Calc { static int add(int a, int b) { return a + b; }"
" public static void main(String[] x) { System.out.println(add(20, 22)); } }")
subprocess.run([os.path.join(JDK, "javac.exe"), "Calc.java"], cwd=d, check=True)
path = os.path.join(d, "Calc.class")
data = bytearray(open(path, "rb").read())
pattern = bytes([0x1A, 0x1B, 0x60, 0xAC]) # iload_0 iload_1 iadd ireturn
at = data.find(pattern)
print("original run:", subprocess.run([os.path.join(JDK, "java.exe"), "-cp", d, "Calc"], capture_output=True, text=True).stdout.strip())
data[at + 2] = 0x62 # iadd → fadd
open(path, "wb").write(data)
r = subprocess.run([os.path.join(JDK, "java.exe"), "-cp", d, "Calc"], capture_output=True, text=True)
print("patched run exit status:", r.returncode)
print("\n".join(line for line in r.stderr.splitlines()[:4] if line.strip()))original run: 42
patched run exit status: 1
Error: Unable to initialize main class Calc
Caused by: java.lang.VerifyError: Bad type on operand stack
Exception Details:Line-by-Line Explanation
add: iload_0 and iload_1 push the two int parameters from local slots 0 and 1; iadd pops both and pushes the sum; ireturn returns it. Static methods have no this, so parameters start at slot 0.
main: getstatic pushes System.out; bipush 20 and bipush 22 push small constants; invokestatic #13 calls add:(II)I; invokevirtual calls println(int).
Header: magic CAFEBABE, major version 65 = Java 21.
Verification: the patched method applies fadd to int operands; the verifier detects the type mismatch at class-loading time and the JVM throws VerifyError before main runs.
Expected Output
As shown; constant-pool indices and counts depend on the compiler.
Step-by-Step Runtime Behavior
main frame: stack [PrintStream] → [PrintStream, 20] → [PrintStream, 20, 22] → call add (new frame; locals [20, 22]; stack [20] → [20, 22] → [42]; return) → [PrintStream, 42] → println → [] → return.
Common Misconceptions
"The JVM runs Java." It runs class files — from Java, Kotlin, Scala, Clojure and others.
"Bytecode verification is optional." Modern JVMs always verify classes outside the platform's trusted set (and -Xverify:none is deprecated).
"javap output is the same everywhere." It depends on the javac version (string concatenation became invokedynamic in Java 9).
Common Bugs and Failure Modes
Running class files compiled for a newer Java on an older JVM (UnsupportedClassVersionError), binary incompatibilities after a library update (NoSuchMethodError), class-loader leaks in application servers, mixing class path and module path incorrectly.
Corrected or Safer Patterns
Compile with --release N for the oldest target JVM; rebuild dependents after API changes; prefer the module path for strong encapsulation; inspect with javap when behaviour surprises you.
Debugging and Inspection Techniques
javap -c -v -p, -Xlog:class+load, jcmd, JFR, async-profiler, helpful NPE messages, -XX:+PrintCompilation.
Performance Considerations
Warm-up from interpretation to C2; class-data sharing for start-up; allocation and GC choice; megamorphic interface calls.
Security Considerations
Verification prevents type confusion from malformed class files; it does not make untrusted code safe to run — isolate untrusted code in separate processes; avoid Java deserialisation of untrusted input.
Portability and Version Caveats
Class-file versions, -XX flags, collectors and JIT behaviour vary across JDK versions and vendors; the class-file format and instruction semantics are specified and stable.
Real-World Use Cases
Server applications, Android (which converts class files to DEX), build tools (Gradle), big-data systems (Spark, Kafka), bytecode instrumentation agents (APM tools, coverage).
Best Practices
Target an explicit release, keep JVM flags documented, monitor GC and JIT logs in production, never load untrusted classes in-process.
Exercises
E1 · Beginner. What does the descriptor (II)I mean?
Answer: A method taking two int parameters and returning an int.
E2 · Intermediate. Why is the first local of add slot 0, while instance methods start their parameters at slot 1?
Answer: Instance methods receive this in slot 0; static methods have no receiver.
E3 · Advanced challenge. Why would javap of the same "a" + b expression differ between Java 8 and Java 21 compilers?
Answer: Java 8's javac emitted StringBuilder append chains; since Java 9 (JEP 280) it emits invokedynamic to StringConcatFactory.makeConcatWithConstants, letting the runtime choose the strategy.
Quiz
? Which JVM instruction adds two ints on the operand stack?
+ iadd :: Typed integer addition.
- add :: That is CIL's untyped add.
- ADD :: MiniLang.
- i32.add :: WebAssembly.
? When did the patched class (fadd on int operands) fail?
+ At class-loading time, during verification. :: VerifyError before main ran.
- When add() returned. :: It never ran.
- At compile time. :: javac produced a valid file; it was altered afterwards.
- Never; the JVM converted the ints. :: The verifier rejects it.
? Which are part of a class file? (select all that apply)
+ A constant pool :: Indexed constants and references.
+ Method bytecode with an exception table :: In the Code attribute.
- JIT-compiled machine code :: Produced at run time, not stored.
+ Line-number tables :: Debug metadata.Summary and Key Takeaways
The JVM executes verified, versioned class files: constant pools, typed stack bytecode, exception tables and debug metadata, loaded lazily through delegating class loaders, interpreted and JIT-compiled by an implementation such as HotSpot. Inspect with javap, rely on the specification, and treat -XX flags as implementation details.
Further Reading
*The Java Virtual Machine Specification*, Java SE 21 Edition, ch. 2–6.
*The Java Language Specification*, §12 "Execution".
Oracle, "Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide".