Java Bytecode Fundamentals

Explore the low-level representation of Java code: op codes, the stack-based JVM architecture, and local variable table mechanics.

published: reading time: 27 min read author: GeekWorkBench

Java Bytecode Fundamentals

Every Java developer works with .class files without always understanding what happens inside them. Bytecode is the intermediate representation that sits between your Java source code and the native machine code that actually runs on the CPU. Understanding bytecode gives you insight into how the JVM thinks, why certain code patterns perform better than others, and how the JIT compiler makes its optimization decisions.

Let’s dig into the fundamental building blocks of JVM bytecode: op codes, the stack-based execution model, and the local variable table.

Introduction

Every .class file contains bytecode—an intermediate representation that sits between your Java source code and the native machine code that runs on the CPU. Most developers interact with bytecode only through exceptions and stack traces, never directly reading what the compiler emits. But understanding bytecode gives you a window into how the JVM thinks, why certain code patterns perform better than others, and how the JIT compiler makes its optimization decisions. When your carefully written code does not perform as expected, or when a profiler points to a hot method that looks innocent in source, bytecode literacy is what separates the developers who understand why from those who just accept the results.

The JVM is a stack-based machine—a deliberate design choice for portability. Unlike x86 or ARM architectures that use registers for most operations, the JVMoperand stack serves as the execution context for every method. Instructions like iadd or aload pop their operands from the stack and push results back, without needing to encode register specifiers in each instruction. This makes bytecode compact (one-byte op codes) and portable across any hardware that has a JVM implementation. It also means the local variable table and operand stack are fundamental to understanding what any bytecode sequence actually does.

This post covers the fundamental building blocks of JVM bytecode: the stack-based execution model, the instruction set organized by category, and how the local variable table maps to method parameters and local variables. You will learn to read javap output, understand how long and double values occupy two stack slots, and see why the bytecode verifier exists as a security boundary between untrusted code and the runtime. Practical examples show how high-level Java code compiles to specific bytecode sequences, and how understanding these sequences explains JIT optimization patterns.

When to Use Bytecode Analysis

Bytecode inspection becomes valuable when you need to:

  • Verify compiler output — Confirm that your high-level Java code compiles to efficient bytecode sequences
  • Debug performance issues — Identify unnecessary object allocations, missed monomorphic calls, or suboptimal synchronization
  • Understand library behavior — Inspect what bytecode a framework generates for proxies, lambdas, or method references
  • Prepare for JVM certification exams — OCP certification tests deep knowledge of bytecode and the JVM specification
  • Detect security issues — Malicious bytecode manipulation sometimes hides in compiled classes

When Not to Use Bytecode Analysis

Bytecode analysis is overkill for:

  • Day-to-day application development — Most developers never need to read bytecode directly
  • Initial performance tuning — Profile first, then investigate bytecode only if profiling points to specific hot spots
  • Simple debugging tasks — Logger statements and unit tests solve most problems faster

JVM Stack-Based Architecture

Unlike native CPU architectures that use registers for most operations, the JVM is fundamentally stack-based. Every method owns a stack frame that contains the operand stack, local variables, and reference data the method needs to execute.

flowchart TD
    subgraph Method["Method Stack Frame"]
        direction TB
        LV["Local Variables\n[0] this\n[1] arg1\n[2] arg2\n[n] temp..."]
        OS["Operand Stack\npush/pop\niadd, aload, ifeq..."]
        CR["Constant Pool\nRuntime Constant Pool Index"]
    end

    subgraph JVM["JVM Execution Engine"]
        INT[Interpreter]
        JIT[JIT Compiler]
    end

    LV <--> OS
    OS --> INT
    OS --> JIT
    INT -.->|hot code| JIT

Why Stack-Based?

The stack-based design achieves several goals that registers cannot:

  1. Portability — Register-based architectures vary wildly across hardware (x86 has 8 general-purpose registers, ARM has 16, RISC-V has 32). A virtual stack abstracts away these differences, making bytecode portable by design.

  2. Compact instruction encoding — Stack instructions are typically one byte (hence “bytecode”) because they do not need to encode register specifiers. An iadd instruction always pops two ints and pushes the result — the JVM knows exactly what to do without additional operands.

  3. Simple verifier — The type system works naturally with a stack because every value has a declared type. The verifier can track stack depth and types at each program point without complex data-flow analysis.

Operand Stack Deep Dive

The operand stack operates on a LIFO (last-in, first-out) model. Each slot on the stack holds a value of a specific type: int, float, reference, long, or double. Long and double values occupy two stack slots.

Consider this simple Java method:

public int add(int a, int b) {
    return a + b;
}

The bytecode for this method looks like:

iload_1       // Push local variable 1 (a) onto stack
iload_2       // Push local variable 2 (b) onto stack
iadd          // Pop two ints, push their sum
ireturn       // Return the int on top of stack

The i prefix on each instruction denotes an int operation. The JVM uses consistent naming: i for int, l for long, f for float, d for double, a for reference, and b for byte/boolean.

Op Codes: The JVM Instruction Set

The JVM specification defines approximately 200 op codes (operations codes). Each op code is a single byte, giving the JVM its name. Some instructions take no operands (like iadd), while others embed data directly into the instruction stream.

Instruction Categories

CategoryExamplesPurpose
Load/Storeaload, astore, iload, istoreMove values between locals and operand stack
Arithmeticiadd, isub, imul, idiv, iremPerform calculations on numeric types
Type Conversioni2l, i2f, i2d, l2iCast between primitive types
Object Operationsnew, getfield, putfield, monitorexitCreate objects and access fields
Control Flowifeq, ifne, if_icmpge, gotoBranch based on stack values
Method Invocationinvokevirtual, invokestatic, invokespecialCall methods
Returnireturn, areturn, returnExit method with or without value

Quick Reference: Load and Store

The JVM provides optimized forms for common load/store patterns. Rather than always using the two-byte iload <index>, the bytecode emits single-byte variants when the index is small.

iload  <n>   // General form: load int from local variable n
iload_0       // Optimized: load from local variable 0
iload_1       // Optimized: load from local variable 1
iload_2       // Optimized: load from local variable 2
iload_3       // Optimized: load from local variable 3

The same optimization pattern applies to astore, fload, dload, and aload. Using _0 through _3 saves one byte per instruction and is faster to decode.

Local Variable Table

Every method stack frame includes a local variables array. The first few slots have special meaning: slot 0 holds this for instance methods, and slots 1 through N hold the method parameters in order. After the parameters, the remaining slots are available for local variables the method declares.

How the Local Variable Table Works

Given this Java method:

public class Calculator {
    public long square(int x) {
        long result = (long) x * x;
        return result;
    }
}

The local variable table looks like:

SlotVariableType
0thisCalculator (reference)
1xint
2resultlong (occupies slots 2 and 3)

The bytecode sequence:

iload_1           // Load x onto stack
i2l               // Convert int to long (consumes 1 slot, produces 2)
iload_1           // Load x again
i2l               // Convert to long
lmul              // Multiply two longs
lstore_2          // Store result into slots 2 and 3
lload_2           // Load result from slots 2 and 3
lreturn           // Return it

Long and Double: Two-Slot Values

A long or double primitive occupies two consecutive local variable slots. The JVM does not define which slot holds the high bits, but the value occupies two positions. When lload_2 loads a long, it loads both slot 2 and slot 3 together.

If you have many long values, your local variable table grows faster than you might expect, and the JIT compiler has more register pressure to manage.

Production Failure Scenarios

Bytecode-level issues show up in production in subtle ways:

1. Stack Overflow from Deep Recursion

The JVM stack has a fixed size per thread (typically 1MB on modern JVMs). Infinite recursion causes StackOverflowError:

public static int factorial(int n) {
    return n * factorial(n - 1);  // Missing base case
}

Each recursive call pushes a new stack frame with at least the local variables and operand stack. After thousands of calls, the stack is exhausted. The error message shows the thread name and stack trace but rarely points to the bytecode directly.

2. Verifier Rejection of Corrupted Class Files

If a class file has been manually edited or corrupted, the bytecode verifier rejects it at load time with ClassFormatError. The verifier checks that instructions reference valid constant pool indices, that the operand stack never overflows or underflows, and that types on the stack match what instructions expect.

3. Unexpected Deoptimization Due to Type Profile Pollution

The JIT compiler maintains type profiles for virtual calls. If a method receives many different receiver types over time, the type profile becomes “polymorphic” and the JIT falls back to a slower dispatch mechanism. This sometimes surprises developers who expect a method to be as fast as a direct call.

Trade-Off Table

AspectStack-Based DesignRegister-Based Design
PortabilityHigh — no register assumptionsLow — tied to target architecture
Instruction encodingCompact (1 byte op codes)Larger — must encode register specifiers
Execution speedSlower for simple ops (more memory accesses)Faster for simple ops (registers)
Verifier complexityLowerHigher
JIT complexityLowerHigher

Implementation Snippets

Inspect Bytecode of a Class

Use javap, the Java class file disassembler:

# Basic bytecode listing
javap -c com.example.MyClass

# Verbose output with local variable table
javap -c -v com.example.MyClass

# Show line numbers and local variable table
javap -c -l -p com.example.MyClass

Example Output from javap

$ javap -c -v com.example.Calculator
public long square(int);
  descriptor: (I)J
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=2
    0: iload_1
    1: i2l
    2: iload_1
    3: i2l
    4: lmul
    5: lstore_2
    6: lload_2
    7: lreturn

The stack=2, locals=3 annotation tells you the maximum operand stack depth (2) and the number of local variables (3: this, x, and result).

Observability Checklist

When analyzing bytecode in production or pre-production, check:

  • Maximum stack depth — Does the bytecode verify that stack never exceeds available space?
  • Local variable usage — Are locals properly initialized before use?
  • Exception handlers — Are catch blocks present and correctly structured?
  • Type constraints — Does the verifier approve the bytecode without errors?
  • Line number table — Is debug information present for stack traces?

Security Notes

Bytecode can be decompiled and inspected. Do not store sensitive data in bytecode or expect compiled classes to hide implementation details. Tools like Procyon, CFR, and Fernflower reconstruct readable Java source from bytecode with high accuracy.

If you need to protect intellectual property in bytecode, use an obfuscator like ProGuard or DashO. Obfuscators rename classes and methods to meaningless identifiers and remove debug information, making reverse engineering significantly harder.

Never load bytecode from untrusted sources. The bytecode verifier provides only basic safety guarantees; a malicious class file can trigger denial-of-service conditions, exhaust memory, or trigger JVM bugs in the JIT compiler.

Common Pitfalls / Anti-Patterns

  1. Assuming bytecode matches source code — The compiler is allowed to reorder instructions, eliminate dead code, and perform constant folding. Bytecode is not a faithful representation of source.

  2. Ignoring two-slot values — Forgetting that long and double occupy two stack slots or local variable slots leads to off-by-one errors in stack map calculations.

  3. Not accounting for stack layout after branching — The verifier tracks the stack type and depth at every instruction. If a branch target has inconsistent stack state depending on which path reached it, verification fails.

  4. Assuming _0 through _3 variants exist for all instructions — Only load/store instructions have these optimized forms. Other instructions like iadd always work on the top of the stack regardless.

  5. Forgetting this in the local variable table — Instance methods always have this at local variable 0. Static methods do not. If you call invokespecial on a constructor, this must be initialized before the constructor completes.

Quick Recap Checklist

  • JVM uses a stack-based architecture for portability and compact encoding
  • Bytecode instructions are one byte op codes with optional operands
  • Operand stack is LIFO; long and double occupy two slots
  • Local variable table stores method parameters at slots 0, 1, 2… (slot 0 is this for instance methods)
  • Use javap -c -v to inspect bytecode of any .class file
  • The bytecode verifier ensures type safety and stack integrity before execution
  • Bytecode can be decompiled — do not rely on it for IP protection
  • Stack overflow causes StackOverflowError; deep recursion is the common cause

Interview Questions

1. Why does the JVM use a stack-based architecture instead of register-based?

The JVM prioritizes portability over raw execution speed. Register-based architectures differ significantly across hardware — x86, ARM, RISC-V, and others all have different numbers and types of registers. A virtual stack abstracts away these differences, allowing the same bytecode to run on any platform with a JVM implementation. Additionally, stack-based instruction encoding is more compact (one byte op codes without register specifiers), which reduces class file size and speeds up interpretation at startup.

2. What is the difference between `iload` and `iload_0` through `iload_3`?

iload <n> is the general form that takes a one-byte index operand, allowing access to any local variable slot from 0 to 255. iload_0, iload_1, iload_2, and iload_3 are optimized short forms that have no operand byte — the slot index is encoded directly in the instruction. This saves one byte per instruction and simplifies the decoder. The JVM uses these optimized forms automatically when the slot index is small enough.

3. Why do long and double values occupy two slots in the local variable table and on the operand stack?

Long and double are 64-bit values, while each stack slot or local variable slot is 32 bits wide. The JVM specification requires these two-slot values to occupy consecutive slots, but the exact layout is otherwise implementation-defined. This has practical implications: a method with many long parameters or local variables exhausts its local variable table faster, and the operand stack must have at least two slots available before pushing a long or double.

4. What does the bytecode verifier do, and what happens if it fails?

The bytecode verifier performs static analysis on every class file before the JVM executes it. It checks that instructions reference valid constant pool entries, that the operand stack never exceeds its maximum depth and never contains type mismatches, that local variables are initialized before use, that every instruction has valid op codes and operands, and that method calls provide the correct number and type of arguments. If verification fails, the JVM throws a VerifyError (or ClassFormatError for malformed class files) and refuses to load the class.

5. What is the purpose of the constant pool in a class file?

The constant pool is a table of constants — numeric literals, string literals, class and method references, field references, and other values — that the bytecode instructions reference by index. Rather than embedding large values like fully qualified class names or method signatures directly in instruction operands (which would make those instructions variable-length), the JVM stores these values once in the constant pool and instructions point to indices. This keeps instruction encoding simple and compact while allowing the JVM to validate all referenced classes and methods at load time.

6. How does the local variable table handle the `this` reference in instance methods, and why is it important for constructors?

Instance methods always reserve local variable slot 0 for the this reference, while static methods do not. This means an instance method with zero parameters still has args_size=1 in its stack frame. For constructors, this must be initialized before any other action occurs — the first instruction in any constructor must be either aload_0 (to access this) or a call to another constructor via invokespecial. If you attempt to use a local variable before the bytecode verifier confirms it has been assigned, verification fails with a VerifyError. The placement of this at slot 0 also explains why invokespecial on a constructor passes the receiver implicitly — there is no explicit aload_0 before the invokespecial to another constructor.

7. What is the relationship between bytecode instruction encoding and the stack-based model?

The stack-based model enables compact one-byte instruction encoding because instructions do not need to encode register operands. An iadd instruction always pops two ints from the operand stack and pushes their sum — the operation is self-contained and needs no additional operands. A register-based architecture would need to specify source and destination registers, which requires additional bytes per instruction. This compactness is why JVM class files are relatively small even for complex applications, and why the interpreter can begin executing quickly without needing to decode complex instruction formats. The tradeoff is that stack-based execution requires more memory accesses per computation because values must be pushed and popped from the stack rather than remaining in registers.

8. Why does the JVM provide optimized single-byte forms like `iload_0` through `iload_3`?

The optimized short forms (iload_0, iload_1, iload_2, iload_3, and the equivalent for aload, fload, dload, astore) encode the local variable slot index directly in the instruction byte rather than requiring a separate operand byte. This saves one byte per instruction. For methods with tight loops or frequently accessed locals, these savings accumulate. The JVM specification mandates that these optimized forms be used automatically when the slot index is 0-3, so compilers emit them by default for common cases. The savings are modest per instruction but meaningful in hot code paths where the same instruction executes millions of times.

9. What is a stack map frame and why does the JVM require it for bytecode verification?

A stack map frame is a compact type annotation embedded in a class file that lists the expected type of each operand stack slot and local variable at a specific branch target instruction. Rather than recomputing types at every instruction during verification, the JVM pre-computes type information at branch targets and stores it as stack map frames. Each frame encodes the type of this (if an instance method), each local variable, and the operand stack maximum depth and types. The verifier uses these frames to quickly check that incoming control flow has consistent types. Stack map frames were made mandatory in Java 7 (class file version 51) because they dramatically speed up verification, which previously required expensive data-flow analysis on every branch target.

10. How does the JVM handle two-slot values like `long` and `double` on the operand stack during verification?

Each operand stack slot holds 32 bits, so a 64-bit long or double occupies two consecutive stack slots. The verifier tracks stack depth in slots, not in values — a long on the stack contributes 2 to the tracked depth. When verifying a dup or swap instruction that manipulates the stack, the verifier must account for two-slot values correctly. For example, dup cannot duplicate a 64-bit value with a single dup instruction because dup operates on a single stack slot. Instead, dup2 handles the two-slot case. For local variables, a long at slot n occupies both n and n+1, and the verifier must ensure no other value uses either of those slots simultaneously.

11. What is the maximum depth of the operand stack, and how is it determined during verification?

The maximum operand stack depth for a method is computed by the bytecode verifier during class loading. The verifier performs data-flow analysis that tracks the type and depth of the operand stack at every instruction point. For each instruction, it computes the outgoing stack state from the incoming state. Branch instructions must have compatible stack states regardless of which path reaches the target — if one branch leaves an int on the stack and another leaves a reference, verification fails. The maximum depth across all instruction points is recorded in the method's Code attribute as stack=n. The verifier also checks that at every point, the stack has enough room for the instruction's operands — for example, iadd requires at least two int slots before it can execute.

12. Why can the same instruction have different effects depending on the types on the stack?

The JVM uses a single opcode for operations that work on multiple types — for example, iadd adds two ints, ladd adds two longs, and dadd adds two doubles. The operand types on the stack determine which operation actually occurs. There is no add instruction that figures out the type at runtime; instead, the compiler emits the type-specific instruction based on what it knows at compile time. This design keeps instruction decoding simple and fast — the JVM knows exactly what to do from the opcode alone, without needing to inspect stack types. Type confusion would cause verification failure, so a well-formed class file always has the correct type on the stack for each instruction.

13. How does the constant pool index work in bytecode instructions, and what happens if the index is invalid?

Many bytecode instructions contain a one-byte or two-byte constant pool index that points into the class file's constant pool table. For example, invokevirtual #42 means "resolve method at constant pool entry 42." During verification, the JVM checks that this index is within bounds and that the entry exists with the expected type — a CONSTANT_Methodref_info for invokevirtual, not a field reference. If the index is out of bounds or the entry has the wrong type, verification fails with ClassFormatError or NoSuchMethodError at link time. If the class file is corrupted or manually edited, the verifier rejects it because it cannot safely resolve references. The constant pool is also where the JVM stores string constants, integer literals, and class names that instructions reference.

14. What is the difference between `areturn` and `return`, and when must each be used?

areturn returns a reference type value from a method and requires that a reference be on the operand stack. It is used when a method's return type is a class, interface, or array type — the caller receives the reference on the stack and typically stores it to a local variable. return with no operand terminates a void method — constructors, methods declared with void return type, and any block that completes without a value. Using areturn when the method is declared void causes a verification error because the verifier expects the operand stack to be empty at a return instruction. Using return when the method should return a value also fails verification. Constructors always use plain return because they return void — the this reference is implicitly passed by the caller, not returned from the constructor.

15. How does the JVM's use of one-byte op codes affect the total size of a class file?

The one-byte opcode design limits the total instruction set to 256 instructions (0-255), but it also means each instruction is compact — just the opcode byte for simple instructions, or the opcode plus operands for complex ones. Most stack-manipulation instructions (iload_0, istore_1, aconst_null, pop) fit in a single byte. Instructions that need an index or constant operand use additional bytes — aload takes two bytes (opcode + index), and bipush takes two bytes (opcode + signed byte). Despite these extra bytes, bytecode is much more compact than native code because there are no register specifiers and most operations target the top of the stack. This compactness matters for large applications with many classes — a typical JAR file is significantly smaller than the equivalent native binary would be.

16. What happens during the two phases of bytecode verification — structural analysis and data-flow analysis?

Bytecode verification proceeds in two distinct phases. The structural analysis phase checks the raw class file format: that constant pool entries have valid tags and well-formed contents, that field and method descriptors use valid type strings, that no instruction exceeds the code array bounds, that every instruction's operands are valid constant pool indices or local variable indices, that each instruction's stack frame has enough operands for the instruction to consume. The data-flow analysis phase then simulates execution by tracking types through the bytecode, verifying that at every instruction, the stack and local variables have types consistent with what the instruction expects. This phase also checks that all exception handlers cover the correct range and that constructors properly initialize this before any other operation. If either phase fails, the class is rejected with a VerifyError.

17. How does the local variable table interact with method parameters for static versus instance methods?

For instance methods, local variable slot 0 holds the this reference, and method parameters start at slot 1. For a method like public int add(String name, int value), slot 0 is this, slot 1 is name (reference), and slot 2 is value (int). For static methods, there is no this, so parameters start at slot 0. A static method public static int max(int a, int b) uses slot 0 for a and slot 1 for b. The verifier checks that aload_0 (which loads local variable 0) is only used in instance methods, never in static methods — using aload_0 in a static method causes verification failure because the verifier knows no this exists at slot 0.

18. Why does the JVM use the value stack for computation instead of local variables for all operations?

Using a stack instead of locals for intermediate values keeps the instruction set uniform and compact. With a stack, an instruction like iadd always knows its operands are on the stack — it does not need to encode which local variable slots to read. This design minimizes instruction size and simplifies the decoder. For complex expressions like (a + b) * (c - d), the JVM computes a + b and pushes the result, then computes c - d and pushes that, then multiplies — each step uses stack slots as scratch space. A register-based design would need to specify source and destination registers in each instruction, which would increase instruction size and complicate register allocation. The tradeoff is more stack traffic (push/pop operations) compared to register-based execution where values can stay in registers between operations.

19. What is the relationship between bytecode instructions and the `javap -c` output notation?

javap -c prints bytecode in a human-readable assembly format where each instruction is shown with its name and any operands. For example, iload_1 means "load int from local variable 1," bipush 10 means "push signed byte 10 onto the stack," and invokevirtual #12 means "call virtual method using constant pool entry 12." The constant pool index is shown as a number prefixed with #. Operands that are local variable indices appear directly after the instruction name (like iload 5 for local variable 5). The offset in brackets (like 0:, 1:) shows the byte offset of each instruction in the code array. The stack= attribute shows the maximum operand stack depth, and locals= shows the number of local variable slots used by the method.

20. How does the bytecode verifier handle branch instructions that merge control flow from different paths?

When two different control flow paths converge at the same instruction (a merge point), the verifier must ensure both paths leave the operand stack in a compatible state. If one path leaves an int on the stack and another leaves a reference, the verifier rejects the bytecode because the instruction at the merge point would not know what type to expect. The verifier checks that both paths produce the same type at the same stack depth. For example, consider a branch if (condition) push int 1 else push reference null — if both branches converge at the same point, this is invalid because the stack types differ. This merge checking is why the verifier fails on code that appears to work in simple cases but would crash at runtime due to type confusion. Exception handlers are also merge points — the handler's stack always starts with the exception object on the stack.

Further Reading

Bytecode Verification Deep Dive

The bytecode verifier is one of the JVM’s most important security boundaries. It performs data-flow analysis to ensure that at every instruction point, the operand stack has the correct type and depth, and that local variables are properly initialized before use. The verifier operates in phases: first it checks structural integrity (valid op codes, valid constant pool references), then it performs type inference by tracking stack and local types through branch paths. If two different control flow paths reach the same instruction with inconsistent stack types, verification fails. This catch-all protection prevents entire classes of exploits where a malicious class file might otherwise crash the JVM or corrupt memory.

Stack Map Frames and Type Checking

Stack map frames are compact type annotations embedded in the bytecode that accelerate verification. Instead of recomputing the type of every stack slot and local variable at each instruction during execution, the verifier pre-computes type information at branch targets and stores it as stack map frames. These were mandatory in Java 7 and later for all class files targeting version 51+. Understanding stack map frames helps you interpret the stack= output from javap -c -v — when you see stack=2, that is the verified maximum stack depth at that instruction point.


Conclusion

Now that you understand JVM bytecode fundamentals, you can trace how your Java source code actually executes under the hood. Use javap -c -v to inspect class files, watch for stack-based operand flow, and recognize how the JVM verifier enforces type safety. This knowledge becomes essential when debugging performance issues, analyzing compiler output, or preparing for JVM certification exams. Continue exploring with the Common Bytecode Instructions guide to master the most frequently used op codes.

Category

Related Posts

Common Bytecode Instructions

Master the most frequently used JVM bytecode instructions: aload, astore, invoke, return, and arithmetic operations.

#java #bytecode #jvm

Method Invocation Bytecode

Deep dive into JVM method invocation: invokevirtual, invokestatic, invokespecial, invokeinterface, and invokedynamic explained per the JVM Specification.

#java #bytecode #jvm

JVM Bytecode Verification: Type Checking and Stack Map Frames

A technical deep dive into the JVM bytecode verifier, covering type checking, stack map frames, the four verification stages, and what happens when verification fails.

#java #jvm #bytecode