JVM Architecture in Java

The class loader, the runtime data areas and the execution engine, and how a class file becomes running machine code.

The three parts

  .class files
       |
       v
+------------------+   +---------------------------+   +------------------+
| Class Loader     |->| Runtime Data Areas          |<-| Execution Engine |
| load, link, init |   | heap, stacks, metaspace     |   | interpreter, JIT |
+------------------+   +---------------------------+   +------------------+
                                                              |
                                                              v
                                                      native instructions

1. The class loader subsystem

It finds a class file, verifies it, prepares it and initialises it. This is covered in detail in the note on class loading; the summary is that classes arrive lazily, on first use, and never before.

2. Runtime data areas

AreaSharedHolds
HeapYesEvery object and array
MetaspaceYesClass metadata, method bytecode, static fields
JVM stackOne per threadFrames: locals, operands, the return address
Program counterOne per threadThe address of the current instruction
Native method stackOne per threadFrames for native code
Code cacheYesMachine code produced by the JIT
public void process() {
    int count = 5;                     // on this thread stack
    String label = "total";            // reference on the stack, object on the heap
    List<String> items = new ArrayList<>();   // reference on the stack, object on the heap
}
The primitive value and the reference live in the frame; the object always lives on the heap. When the method returns, the frame is discarded and the object survives only if something else still refers to it.

3. The execution engine

The interpreter

Reads bytecode one instruction at a time and performs it. Starting is immediate, but each instruction is re-decoded on every pass.

The just in time compiler

The JVM counts how often each method and loop runs. Once a threshold is crossed, the method is compiled to native code and future calls use the compiled version. This is why a Java process becomes faster after it has been running for a while.

call 1..n      interpreted, counters increasing
threshold      C1 compiles quickly, with basic optimisation
still hot      C2 compiles slowly, with aggressive optimisation
assumption
broken         deoptimise back to the interpreter, then recompile

Tiered compilation

Modern JVMs use both compilers: C1 gives quick improvement, and the code that stays hot is recompiled by C2 with far more optimisation. The JVM also gathers profile data while interpreting, which lets it make assumptions a static compiler cannot, such as that a call site only ever sees one implementation.

The stack in detail

public int calculate() {
    return add(2, 3);
}

private int add(int a, int b) {
    return a + b;
}
Thread stack (grows downwards)
+-----------------------+
| calculate() frame     |  locals: this
+-----------------------+
| add() frame           |  locals: this, a=2, b=3
+-----------------------+  operand stack used for the addition

Each frame holds a local variable array, an operand stack for intermediate values, and a reference to the constant pool of its class. Exceeding the stack depth, usually through unbounded recursion, throws StackOverflowError.

Bytecode

public int add(int a, int b) {
    return a + b;
}
iload_1        push a
iload_2        push b
iadd           pop two, add, push the result
ireturn        return the top of the operand stack

The JVM is a stack machine: operands are pushed and popped rather than held in named registers. Inspect any class with javap -c ClassName.

Useful command line tools

ToolPurpose
javap -cDisassemble bytecode
jpsList running JVMs
jcmdA general diagnostic command interface
jstackThread dump
jmapHeap summary and histogram
jstatLive garbage collection statistics
jps -l
jcmd <pid> VM.flags
jcmd <pid> GC.heap_info
jstack <pid> > threads.txt

Common startup flags

java -Xms512m -Xmx2g            initial and maximum heap
     -Xss1m                     stack size per thread
     -XX:+UseG1GC               choose the collector
     -XX:+HeapDumpOnOutOfMemoryError
     -XX:MaxMetaspaceSize=256m
     -jar application.jar

The JVM is a specification

HotSpot is the common implementation, but the JVM is a document that anything may implement. That is also why languages other than Java can target it: they simply emit valid class files.

Common misconceptions

  • Java is slow because it is interpreted. Hot code is compiled to native instructions at runtime.
  • The JVM is the same as the JDK. The JVM executes bytecode; the JDK is the whole development kit.
  • Objects can live on the stack. Only frames and their contents do; escape analysis may avoid an allocation, but that is an optimisation, not a rule you can rely on.
  • More heap is always better. A larger heap can mean longer pauses when it is collected.

Practice

  1. Compile a small class and read its bytecode with javap -c.
  2. Explain which memory area holds a local int, a String reference and the String object.
  3. Why does a long running server often become faster after several minutes?
  4. Use jps and jcmd to inspect a running Java process.
  5. What triggers StackOverflowError, and which area is exhausted?

Conclusion

The JVM loads classes, keeps objects on a shared heap and frames on per thread stacks, and executes bytecode by interpreting it and then compiling the hot parts. Nearly every performance and memory question comes back to that structure.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.