A virtual machine (VM) is a software implementation of a computer system that executes programs in a platform-independent manner. VM architecture defines how these systems are structured, from instruction sets to memory management to execution models.

What is a Virtual Machine?

A VM provides an abstraction layer between code and hardware:

graph TD
    A[Application Code] --> B[Virtual Machine]
    B --> C[Operating System]
    C --> D[Physical Hardware]

    style A fill:#e3f2fd
    style B fill:#fff9c4
    style C fill:#f1f8e9
    style D fill:#fce4ec

Types of Virtual Machines:

  1. Process VMs - Execute single applications (JVM, YARV, Python VM)
  2. System VMs - Virtualize entire systems (VMware, VirtualBox, QEMU)

This note focuses on process VMs used for programming language execution.

Core Components

┌─────────────────────────────────────┐
│         Virtual Machine             │
│                                     │
│  ┌──────────────────────────────┐  │
│  │    Execution Engine          │  │
│  │  - Interpreter / JIT         │  │
│  └──────────────────────────────┘  │
│                                     │
│  ┌──────────────────────────────┐  │
│  │    Memory Management         │  │
│  │  - Stack                     │  │
│  │  - Heap                      │  │
│  │  - Garbage Collection        │  │
│  └──────────────────────────────┘  │
│                                     │
│  ┌──────────────────────────────┐  │
│  │    Runtime System            │  │
│  │  - Object System             │  │
│  │  - Type System               │  │
│  │  - Exception Handling        │  │
│  └──────────────────────────────┘  │
│                                     │
│  ┌──────────────────────────────┐  │
│  │    Standard Library          │  │
│  │  - Built-in Functions        │  │
│  │  - Core Classes              │  │
│  └──────────────────────────────┘  │
└─────────────────────────────────────┘

Architectural Patterns

1. Stack-Based Architecture

The stack-based virtual machine uses a stack data structure for operations:

Example: 2 + 3

Instructions:
  push 2
  push 3
  add

Stack evolution:
  []  →  [2]  →  [2,3]  →  [5]

Examples:

  • YARV (Ruby)
  • JVM (Java)
  • CPython (Python)
  • WebAssembly

Characteristics:

  • Simpler bytecode
  • More instructions
  • Easier to compile to
  • Natural for expression evaluation

See stack-based virtual machine for detailed exploration.

2. Register-Based Architecture

Uses virtual registers instead of a stack:

Example: 2 + 3

Instructions:
  load r1, 2
  load r2, 3
  add r3, r1, r2

Registers:
  r1 = 2
  r2 = 3
  r3 = 5

Examples:

  • Lua VM
  • Dalvik (Android)
  • Parrot VM

Characteristics:

  • Fewer instructions
  • More complex bytecode
  • Harder to compile to
  • Potentially faster execution

Comparison

AspectStack-BasedRegister-Based
InstructionsSimple, numerousComplex, fewer
Bytecode sizeSmallerLarger
Execution speedSlower (more ops)Faster (fewer ops)
Compiler complexitySimplerMore complex
ExamplesYARV, JVMLua, Dalvik

Execution Models

1. Pure Interpretation

Execute bytecode directly without compilation:

┌──────────┐     ┌─────────────┐     ┌──────────┐
│ Bytecode │ ──→ │ Interpreter │ ──→ │ Execute  │
└──────────┘     └─────────────┘     └──────────┘

Advantages:

  • Simple implementation
  • Fast startup
  • Small memory footprint

Disadvantages:

  • Slow execution
  • Repetitive decoding overhead
  • No optimization

Example: Early Python, Ruby 1.8

2. Just-In-Time (JIT) Compilation

Compile hot code paths to native machine code:

┌──────────┐     ┌─────────────┐     ┌──────────┐
│ Bytecode │ ──→ │ Interpreter │ ──→ │ Execute  │
└──────────┘     └──────┬──────┘     └──────────┘
                        │
                   [Profile]
                        │
                        ↓
                ┌───────────────┐
                │  JIT Compiler │
                └───────┬───────┘
                        │
                        ↓
                 ┌─────────────┐
                 │ Native Code │
                 └──────┬──────┘
                        │
                        ↓
                  [Fast Execute]

Advantages:

  • Adaptive optimization
  • Balance startup and runtime speed
  • Profile-guided optimization

Disadvantages:

  • Complex implementation
  • Memory overhead
  • Compilation latency

Examples:

  • YARV with YJIT
  • JVM with HotSpot
  • V8 (JavaScript)

See just-in-time compilation for deeper exploration.

3. Ahead-of-Time (AOT) Compilation

Compile to native code before execution:

┌────────────┐     ┌──────────┐     ┌──────────┐
│ Source/BC  │ ──→ │ Compiler │ ──→ │  Native  │
└────────────┘     └──────────┘     └────┬─────┘
                                          │
                                          ↓
                                    [Fast Execute]

Advantages:

  • Fastest execution
  • No compilation overhead at runtime
  • Smaller runtime

Disadvantages:

  • Slow startup (compilation time)
  • Platform-specific binaries
  • No runtime optimization

Examples:

  • Java GraalVM native-image
  • PyPy with translation toolchain
  • .NET CoreRT

Memory Management

Memory Layout

Virtual Machine Memory:

┌──────────────────────┐ ← High addresses
│   Call Stack         │
│   - Return addresses │
│   - Local variables  │
│   - Function frames  │
├──────────────────────┤
│   Value Stack        │   (Stack-based VMs only)
│   - Operands         │
│   - Intermediate     │
├──────────────────────┤
│   Heap               │
│   - Objects          │
│   - Dynamic data     │
│   - Grows upward     │
├──────────────────────┤
│   Constant Pool      │
│   - Literals         │
│   - Symbols          │
│   - Method metadata  │
├──────────────────────┤
│   Code Area          │
│   - Bytecode         │
│   - JIT compiled     │
└──────────────────────┘ ← Low addresses

Garbage Collection

VMs typically include automatic garbage collection:

Common Strategies:

  1. Mark and Sweep

    • Mark reachable objects
    • Sweep unmarked objects
    • Simple but can pause execution
  2. Generational GC

    • Young generation (frequent, fast)
    • Old generation (rare, slower)
    • Based on object lifetime patterns
  3. Reference Counting

    • Track references to each object
    • Immediate reclamation
    • Struggles with cycles
  4. Concurrent/Incremental

    • GC runs alongside execution
    • Reduces pause times
    • More complex implementation

Ruby’s GC: Generational mark-and-sweep with incremental marking

Instruction Set Design

VM instruction sets balance expressiveness and efficiency:

Instruction Categories

1. Stack/Register Manipulation

push <value>    # Stack-based
load <reg>      # Register-based
pop
dup
swap

2. Arithmetic/Logic

add, sub, mul, div
and, or, not, xor
shl, shr        # Bit shifts

3. Control Flow

jump <offset>
branch_if <offset>
call <method>
return

4. Memory Access

getlocal <index>
setlocal <index>
getfield <name>
setfield <name>

5. Object Operations

new <class>
invoke <method>
getattr <name>
setattr <name>

See YARV stack instructions for concrete examples from Ruby.

Instruction Encoding

Fixed-width encoding:

Each instruction is the same size
Pros: Simple decoding, predictable
Cons: Wastes space for simple instructions

Variable-width encoding:

Instructions vary in size based on operands
Pros: Compact bytecode
Cons: More complex decoding

YARV uses variable-width encoding for compact bytecode.

Optimization Techniques

1. Inline Caching

Cache method lookup results:

# First call: lookup method, cache result
obj.method_name
 
# Subsequent calls: use cached lookup
# (if obj's class hasn't changed)

Performance impact: 2-10x speedup for method calls

2. Constant Folding

Evaluate constants at compile time:

# Source: x = 2 + 3
# Bytecode: x = 5  (computed at compile-time)

3. Peephole Optimization

Optimize small instruction sequences:

# Before:
push 1
push 1
add

# After:
push 2  (specialized instruction)

4. Dead Code Elimination

Remove unreachable code:

if false
  expensive_operation()  # Removed at compile-time
end

5. Method Inlining

Replace method calls with method body:

# Source:
def add(a, b)
  a + b
end
result = add(2, 3)
 
# Inlined:
result = 2 + 3

Examples of VM Architectures

YARV (Ruby)

  • Type: Stack-based virtual machine
  • Bytecode: Variable-width instructions
  • Execution: Interpreter + optional YJIT
  • GC: Generational mark-and-sweep
  • Notable: Focus on simplicity, gradual JIT adoption

See YARV for full details.

JVM (Java)

  • Type: Stack-based
  • Bytecode: .class files
  • Execution: HotSpot JIT compiler
  • GC: Multiple strategies (G1, ZGC, Shenandoah)
  • Notable: Mature optimization, massive ecosystem

V8 (JavaScript)

  • Type: Register-based (internally)
  • Bytecode: Ignition bytecode
  • Execution: TurboFan optimizing compiler
  • GC: Generational with concurrent marking
  • Notable: Aggressive optimization, hot-swapping tiers

LLVM

  • Type: Register-based SSA form
  • Bytecode: LLVM IR (intermediate representation)
  • Execution: Optimization passes + native codegen
  • Notable: Not a VM per se, but compiler infrastructure

Debugging and Profiling

VMs typically provide introspection tools:

Bytecode Disassembly:

# Ruby
ruby --dump=insns -e 'code'
 
# Python
python -m dis script.py
 
# Java
javap -c ClassName.class

Profiling:

# Ruby
ruby --yjit-stats script.rb
 
# Method-level profiling
require 'ruby-prof'
result = RubyProf.profile { code }

Tracing:

# Ruby VM instruction tracing
RubyVM::InstructionSequence.compile_option = {
  trace_instruction: true
}

Performance Characteristics

VM Overhead Factors:

  1. Instruction Dispatch - Decoding and executing bytecode
  2. Memory Indirection - Accessing heap-allocated objects
  3. Dynamic Typing - Runtime type checking
  4. Garbage Collection - Automatic memory management pauses
  5. Method Dispatch - Looking up methods dynamically

Mitigation Strategies:

  1. JIT Compilation - Compile hot code to native
  2. Inline Caching - Cache method lookups
  3. Escape Analysis - Allocate on stack when possible
  4. Concurrent GC - Reduce pause times
  5. Profile-Guided Optimization - Optimize based on usage

Trade-offs

AspectVM ApproachNative Approach
PortabilityExcellentPoor
Startup SpeedFastSlow (if compiled)
Peak PerformanceGood (with JIT)Excellent
Memory UsageHigherLower
DebuggingEasierHarder
SafetyBuilt-inManual
Dynamic FeaturesNaturalDifficult