Supplement · JVM Deep Dive
Java Deep Dive — Study Notes

Java hoạt động như thế nào?

Tài liệu này gom lại toàn bộ mạch học: JDK, JVM, bytecode, Constant Pool, Runtime Constant Pool, String Pool, Class Loading, ClassLoader, JVM Stack, Stack Frame, Heap, static/instance runtime model, Exceptions, GC, Interpreter và JIT.

Mục tiêu: không học thuộc định nghĩa rời rạc, mà hiểu cả journey từ source code đến lúc CPU thực thi.

1. Bức tranh tổng thể

Đúng: đây chính là phần lõi để hiểu cách Java chạy trên JVM.

Main.java
   │
   │ javac
   ▼
Main.class
   │
   ├── Bytecode
   └── Constant Pool
   │
   ▼
java.exe
   │
   ▼
JVM / HotSpot starts
   │
   ▼
Class Loading
   ├── Loading
   ├── Linking
   │   ├── Verification
   │   ├── Preparation
   │   └── Resolution
   └── Initialization
   │
   ▼
main() được gọi
   │
   ▼
JVM Stack / Stack Frame
   ├── Local Variables
   └── Operand Stack
   │
   ▼
Bytecode Execution
   ├── Interpreter
   └── JIT Compiler
          │
          ▼
     Native Machine Code
          │
          ▼
         CPU

Trong lúc chạy:
Heap chứa object
GC thu hồi object unreachable
String Pool quản lý interned String

2. JDK, java.exe và JVM

JDK là bộ công cụ để phát triển và chạy Java. Bạn đang dùng bản JDK 21 portable dạng ZIP là hoàn toàn đủ để học sâu.

JDK
├── bin
│   ├── javac.exe   ← compile .java → .class
│   ├── java.exe    ← launcher, khởi tạo JVM
│   ├── javap.exe   ← disassemble / xem bytecode
│   ├── jcmd.exe
│   ├── jstack.exe
│   └── ...
├── lib
├── jmods
└── ...

java.exe không phải toàn bộ JVM. Nó là launcher. Trên Windows, phần native cốt lõi của HotSpot thường nằm trong file kiểu bin\server\jvm.dll.

Bạn chạy:
java Main

Windows tạo process java.exe
        ↓
java.exe khởi tạo JVM
        ↓
JVM load Main.class
        ↓
gọi Main.main(...)
JVM không cần chạy sẵn. Khi bạn chạy java Main, JVM mới được khởi tạo trong process đó. Thông thường mỗi Java process có JVM instance riêng.

3. Từ .java sang .class

Source Java là text; file .class là binary theo chuẩn JVM Class File Format.

Java source:
int a = 10;
int b = 20;
int c = a + b;

javac
  ↓

Bytecode (human-readable qua javap):
bipush 10
istore_1
bipush 20
istore_2
iload_1
iload_2
iadd
istore_3

javap không phải thứ JVM cần. JVM hiểu binary trực tiếp. javap chỉ giúp con người đọc dạng disassembly.

Bytecode nằm giữa source code và machine code: thấp hơn Java source, nhưng chưa phải instruction của Intel/AMD/ARM.

4. Constant Pool & Symbolic Reference

Mỗi class file có Constant Pool riêng. Nó chứa constants và symbolic references mà bytecode tham chiếu.

Constant pool:
#7  = String      #8          // abc
#9  = Fieldref    ...         // java/lang/System.out
#15 = Methodref   ...         // PrintStream.println:(I)V

Bytecode có thể viết:

ldc           #7
getstatic     #9
invokevirtual #15

Ý tưởng là:

instruction
   ↓
tham chiếu Constant Pool
   ↓
biết constant / class / field / method nào đang được nói tới

Symbolic Reference

Thay vì hard-code địa chỉ memory, class file lưu thông tin kiểu:

java/io/PrintStream.println:(I)V

JVM sẽ resolve symbolic reference thành runtime representation có thể sử dụng để gọi method/field thực tế.

Quan trọng: entry #7 trong Constant Pool không phải Java object, và biến Java không “trỏ vào #7”.

5. String Pool

String Pool là runtime concept dành cho interned strings, đặc biệt là string literals.

String a = "abc";
String b = "abc";

Class file có thể chỉ cần một constant:

#7 = String #8 // abc

Runtime:

Class-file Constant Pool
#7 → "abc"
     │
     │ ldc #7
     ▼
Runtime resolution
     │
     ▼
String Intern Pool
     │
     ▼
canonical String object "abc"
     ▲               ▲
     │               │
     a               b

Nhiều class khác nhau có Constant Pool riêng nhưng vẫn có thể cùng dùng một interned String:

A.class #7  → "abc" ──┐
                       ├──► canonical String "abc"
B.class #15 → "abc" ──┘

new String("abc")

String a = "abc";
String b = new String("abc");

a tham chiếu canonical interned String. b tham chiếu một String object mới.

a ───────────────► String "abc" (interned)

b ───────────────► String "abc" (new object)

Do đó:

a == b       // false
a.equals(b)  // true

Nếu:

String c = b.intern();

thì c sẽ nhận canonical reference tương ứng.

Không phải mọi String object đều nằm trong String Pool. Literal là String object và thường được intern; object tạo bởi new String(...) không tự động trở thành canonical pooled object.

6. Primitive vs Object

Java có 8 primitive types, và chúng không phải Object:

PrimitiveWrapper class
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
int a = 10;        // primitive value
String s = "abc";  // reference tới String object
Integer x = 10;    // reference tới Integer object

Bytecode cũng phản ánh khác biệt:

int:
iload / istore

object reference:
aload / astore

7. Class Loading Lifecycle

Khi JVM tìm thấy Main.class, nó không chạy ngay. Class sẽ đi qua lifecycle:

Loading
   ↓
Linking
   ├── Verification
   ├── Preparation
   └── Resolution
   ↓
Initialization

Loading

Class Loader đọc binary và tạo runtime representation của class.

Verification

JVM kiểm tra class/bytecode có hợp lệ không: format, type consistency, operand stack, control flow...

Preparation

JVM chuẩn bị storage cho static fields và gán default values.

static int count = 100;

Preparation:
count = 0

Initialization:
count = 100

Resolution

Symbolic references được resolve khi cần.

"java/io/PrintStream.println:(I)V"
        ↓
runtime-resolved method reference

Initialization & <clinit>

Static initialization code được gom vào logic class initialization.

static int number = 100;

static {
    System.out.println("Main initialized");
}

Cần phân biệt:

<init><clinit>
instance constructorclass initialization
chạy khi new objectchạy khi class được initialize
có thể chạy nhiều lầnthường một lần / class / class loader

8. JVM Stack & Stack Frame

Mỗi thread có JVM Stack riêng. Mỗi method invocation tạo một Stack Frame.

Thread main
   ↓
JVM Stack

┌────────────────────┐ ← top
│ methodB frame      │
├────────────────────┤
│ methodA frame      │
├────────────────────┤
│ main frame         │
└────────────────────┘

Một Stack Frame quan trọng có:

Stack Frame
├── Local Variables
└── Operand Stack

Ví dụ

static int add(int a, int b) {
    int result = a + b;
    return result;
}
Local Variables:
slot 0 → a
slot 1 → b
slot 2 → result

Operand Stack:
[]
[a]
[a,b]
[a+b]
[]

Khi caller gọi method:

caller Operand Stack
        ↓ arguments
callee Local Variables

Khi method return:

callee return value
        ↓
caller Operand Stack

stack=2 nghĩa là gì?

Không phải số tham số lớn nhất của operation, mà là độ sâu tối đa của Operand Stack tại bất kỳ thời điểm nào trong method.

Recursion & StackOverflowError

hello()
  ↓
hello()
  ↓
hello()
  ↓
...

Mỗi call thêm một frame. Quá nhiều nested frames có thể gây StackOverflowError.

Instance method và this

instance method:
slot 0 → this
slot 1 → first parameter
...

static method:
không có this

9. Heap & Object Reference

Heap là vùng memory dùng chủ yếu cho object và array, chia sẻ giữa các threads.

Person p = new Person();

Stack                       Heap
p: ref ──────────────────► Person object
                            age = 0
                            name = null

Dòng Person p = new Person() nên tách thành hai ý:

new Person()
→ tạo object

p
→ chứa reference tới object đó

Bytecode của new

new
dup
invokespecial <init>
astore

dup duplicate reference trên Operand Stack vì JVM vừa cần reference để gọi constructor, vừa cần giữ reference lại để store vào local variable.

Primitive field vẫn có thể ở trong object

class Person {
    int age;
}

age là primitive nhưng là field của object, nên nó là một phần dữ liệu của object trên Heap.

Hai biến có thể trỏ cùng một object

Person p1 = new Person();
Person p2 = p1;
p1 ───┐
       ├────► Person object
p2 ───┘

Java copy reference value, không copy toàn bộ object.

Java là pass-by-value

Khi truyền object vào method, Java vẫn copy một value — value đó là reference.

main frame:
p1 → ref XYZ

callee frame:
p  → ref XYZ

Nếu callee sửa object qua reference thì caller thấy thay đổi. Nhưng nếu callee gán p = new Person(), caller không bị đổi reference.

10. Garbage Collection

GC quan tâm object còn reachable hay không.

GC Roots

GC bắt đầu từ những root như active stack references, static references, thread-related references, JNI/native references...

GC Roots
   │
   ├────► A ─► B ─► C
   │
   └────► D

X ─► Y   // không có đường từ root tới X/Y

A, B, C, D còn sống. X và Y có thể trở thành garbage.

Circular reference vẫn có thể bị GC

A ───► B
▲      │
└──────┘

Nếu không có đường từ GC Roots tới A/B, cả hai vẫn có thể bị thu hồi.

Young & Old Generation

Heap
├── Young Generation
│    ├── Eden
│    └── Survivor
└── Old Generation

Object mới thường bắt đầu ở Young. Object sống lâu có thể được promote sang Old.

Stop-The-World

Một số GC phase cần tạm dừng application threads để xử lý nhất quán. Collector hiện đại cố giảm pause và làm nhiều việc concurrently.

Memory leak vẫn có thể xảy ra trong Java

static List<Person> people = new ArrayList<>();

while (true) {
    people.add(new Person());
}

Các Person vẫn reachable từ static list, nên GC không được xóa. Đây là memory leak theo nghĩa object không còn hữu ích nhưng vẫn reachable.

StackOverflow vs OutOfMemory

LỗiÝ nghĩa thường gặp
StackOverflowErrorquá nhiều nested stack frames
OutOfMemoryErrorJVM không còn đủ memory cho một loại allocation nào đó; heap exhaustion là case phổ biến

11. Interpreter & JIT Compiler

CPU không hiểu bytecode như iload, iadd, invokevirtual. JVM Execution Engine phải thực thi chúng.

Interpreter

Interpreter đọc và thực hiện bytecode instruction. Ưu điểm: có thể bắt đầu chạy nhanh, không cần compile mọi method trước.

JIT — Just-In-Time Compiler

Khi một method/code path chạy nhiều và trở thành “hot”, JVM có thể compile bytecode đó thành native machine code cho CPU hiện tại.

Bytecode
   ↓
Interpreter lúc đầu
   ↓
profiling
   ↓
hot code
   ↓
JIT compile
   ↓
Native code
   ↓
CPU

Vì sao không compile hết từ đầu?

Compile tất cả sẽ tăng startup cost và có thể phí công cho những method hầu như không chạy.

Warm-up

Ứng dụng JVM có thể chạy nhanh hơn sau một thời gian vì class đã load, references đã resolve, code hot đã được JIT compile và optimize.

JIT không chỉ “dịch”

Nó còn tối ưu dựa trên runtime profile, ví dụ method inlining, speculative optimization và deoptimization khi assumption không còn đúng.

HotSpot, C1, C2

HotSpot tập trung tối ưu các “hot spots”. Với tiered compilation, có thể có các mức compile khác nhau: C1 thiên về compile nhanh, C2 thiên về optimization sâu hơn.

Code Cache

Native code do JIT tạo được giữ trong vùng runtime như Code Cache. Vì vậy JVM memory không chỉ có Stack và Heap.

12. Runtime Constant Pool

Đây là mảnh ghép còn thiếu giữa Constant Pool trong file .class và lúc JVM thực thi class.

Main.class trên disk
└── Class-file Constant Pool
     ├── #7  String "abc"
     ├── #9  Fieldref System.out
     └── #15 Methodref PrintStream.println

           ↓ class được load

JVM Runtime
└── Runtime Constant Pool của Main
     ├── runtime representation của constants
     ├── symbolic references
     └── resolved references khi cần

Class-file Constant Pool là cấu trúc binary nằm trong file .class. Khi class được load, JVM tạo runtime representation tương ứng cho class đó.

Ví dụ bytecode:

ldc           #7
getstatic     #9
invokevirtual #15

Ở runtime, JVM không nhất thiết cứ quay lại đọc file .class trên disk cho mỗi instruction. Class đã được load và JVM có runtime structures để thực thi.

Ba khái niệm cần phân biệt:
Class-file Constant Pool = dữ liệu trong file .class
Runtime Constant Pool = representation của constant pool khi class đã được load
String Intern Pool = nơi quản lý canonical interned String objects
Class-file Constant Pool
          ↓ loading
Runtime Constant Pool
          ↓ resolution / use
runtime entity

String literal constant
          ↓
String Intern Pool
          ↓
canonical String object

13. Metaspace, Code Cache và JVM Memory Areas

JVM memory không chỉ có Stack và Heap. Hai vùng đó rất quan trọng, nhưng HotSpot còn sử dụng nhiều vùng memory khác.

Java Process / HotSpot JVM

├── Heap
│    ├── Objects
│    └── Arrays
│
├── Thread Stacks
│    ├── main thread stack
│    ├── worker thread stack
│    └── ...
│
├── Metaspace
│    └── class metadata
│
├── Code Cache
│    └── native code do JIT compile
│
└── Native / JVM internal memory
     └── runtime structures khác

Metaspace

Metaspace dùng cho metadata của các class đã được load. Ví dụ có thể nghĩ tới thông tin về class, method, field và runtime metadata liên quan.

Nó không nên được hiểu là “nơi chứa mọi static object”. Static fields vẫn là một phần của runtime state của class, còn object được reference bởi static field vẫn là object trên Heap.

Code Cache

Khi JIT compiler compile bytecode thành native machine code, phần native code đó cần được lưu ở đâu đó. HotSpot dùng Code Cache cho mục đích này.

Bytecode
   ↓
JIT
   ↓
Native machine code
   ↓
Code Cache
   ↓
CPU execute
Đừng học thuộc câu “Java memory chỉ có Stack và Heap”. Đúng hơn là Stack và Heap là hai memory areas quan trọng nhất để bắt đầu học; JVM runtime thực tế có nhiều vùng khác.

14. ClassLoader Hierarchy

Trước đây ta nói “ClassLoader load Main.class”. Nhưng thực tế JVM có nhiều class loaders với trách nhiệm khác nhau.

Bootstrap ClassLoader
        ↓
Platform ClassLoader
        ↓
Application ClassLoader

Bootstrap ClassLoader

Load các class nền tảng của Java runtime, ví dụ những class core như java.lang.Object, java.lang.String và nhiều class hệ thống khác.

Platform ClassLoader

Load các platform modules/libraries của Java mà không thuộc phần bootstrap core nhỏ nhất.

Application ClassLoader

Thường là loader load code application của bạn từ classpath, ví dụ Main.class.

Parent Delegation

Mental model đơn giản:

Application ClassLoader muốn load "java.lang.String"
        ↓
hỏi parent trước
        ↓
Platform
        ↓
hỏi parent
        ↓
Bootstrap
        ↓
Bootstrap đã biết String
        ↓
trả class về

Ý tưởng quan trọng là class loader con thường ưu tiên hỏi parent trước thay vì tự load ngay.

Điều này giúp tránh việc application tùy tiện thay thế các core classes của Java.

Trong hệ thống plugin, application server, framework container hoặc custom runtime, ClassLoader có thể phức tạp hơn nhiều. Nhưng Bootstrap → Platform → Application là foundation cần nhớ.

15. static vs Instance ở Runtime

Đây là phần nối Class Loading với Heap.

class Counter {
    static int total = 0;
    int value = 0;
}

total thuộc về class-level state. value thuộc về từng object instance.

Class Counter
└── static total = 0

Heap
├── Counter object A
│    └── value = 10
│
└── Counter object B
     └── value = 20

Nếu:

Counter a = new Counter();
Counter b = new Counter();

a.value = 10;
b.value = 20;

Counter.total = 2;

thì có 2 instance fields riêng nhưng chỉ một class-level static field:

Counter.total = 2

a ──► Counter object A
      value = 10

b ──► Counter object B
      value = 20

static object reference

static Person global = new Person();

Không nên nói “Person object nằm trong static area”. Mental model đúng hơn:

class-level static field
global: reference
        │
        ▼
Heap
┌──────────────┐
│ Person object│
└──────────────┘

Static field giữ reference; object vẫn là object trên Heap.

Preparation vs Initialization

static int total = 100;

Preparation:
total = 0

Initialization:
total = 100

Đây là chỗ class loading lifecycle và static runtime state gặp nhau.

16. Exception và Stack Unwinding

Exception là một ví dụ rất đẹp để thấy Stack Frame thực sự được JVM xử lý thế nào.

static void methodB() {
    throw new RuntimeException("boom");
}

static void methodA() {
    methodB();
}

public static void main(String[] args) {
    methodA();
}

Lúc methodB() throw exception:

JVM Stack

┌────────────────────┐ ← top
│ methodB frame      │
├────────────────────┤
│ methodA frame      │
├────────────────────┤
│ main frame         │
└────────────────────┘

JVM tìm exception handler phù hợp trong method hiện tại. Nếu không có, frame bị unwound và JVM tìm ở caller.

methodB
  ↓ no handler
pop / unwind methodB frame

methodA
  ↓ no handler
pop / unwind methodA frame

main
  ↓ no handler
exception reaches top
  ↓
thread terminates with uncaught exception

try/catch

try {
    methodA();
} catch (RuntimeException e) {
    System.out.println("caught");
}

Compiler ghi exception handling metadata vào class file. javap -c -v có thể cho bạn thấy Exception table.

Exception table:
from   to   target   type
...    ...  ...      java/lang/RuntimeException

Khi exception xảy ra, JVM dùng bảng này để tìm handler phù hợp trong method.

Exception object cũng là object

throw new RuntimeException("boom");

RuntimeException là object. Nó có thể được cấp phát trên Heap giống các object Java khác.

Stack unwinding giải thích vì sao stack trace cho bạn thấy chuỗi method calls: nó phản ánh call stack tại thời điểm exception xảy ra.

17. Mental model cuối cùng

.java
  │
  │ javac
  ▼
.class
  ├── bytecode
  └── constant pool
        │
        ▼
java.exe
  │
  ▼
JVM / HotSpot
  │
  ├── Class Loading
  │    ├── Loading
  │    ├── Verification
  │    ├── Preparation
  │    ├── Resolution
  │    └── Initialization
  │
  ├── Thread Stacks
  │    └── Stack Frames
  │         ├── Local Variables
  │         └── Operand Stack
  │
  ├── Heap
  │    ├── Objects
  │    └── Arrays
  │
  ├── String Intern Pool
  │
  ├── GC
  │    └── reclaim unreachable objects
  │
  └── Execution Engine
       ├── Interpreter
       └── JIT
             ↓
        Native Machine Code
             ↓
            CPU
Đúng, đây là cách Java hoạt động ở mức JVM/runtime. Tuy nhiên đây vẫn là mental model nền tảng: implementation thực tế của HotSpot còn sâu hơn rất nhiều về object layout, compressed oops, TLAB, safepoints, barriers, collector internals, deoptimization, native calls, JMM...

18. Roadmap sau phần “How Java Works”

Nếu đã hiểu tài liệu này, bạn đã có foundation khá đầy đủ về cách Java chạy trên JVM. Bước tiếp theo nên chuyển từ “JVM hoạt động thế nào” sang “Java language/runtime được xây trên JVM thế nào”.

Java Runtime Foundation
        ↓
static / final / object lifecycle
        ↓
inheritance / interface
        ↓
virtual dispatch
        ↓
exceptions
        ↓
collections / generics
        ↓
I/O / NIO
        ↓
threads / synchronized / volatile
        ↓
Java Memory Model
        ↓
reflection / annotations
        ↓
frameworks như Spring

Những phần JVM nâng cao có thể học sau

Object layout
Compressed references
TLAB
Escape analysis
Safepoints
Write barriers
G1 internals
ZGC internals
C1 / C2 deeper
Deoptimization
JNI / native methods
Java Memory Model details
Nếu mục tiêu là Java Backend Developer, không cần học hết JVM internals trước khi sang Collections, Exception, Concurrency và Spring. Foundation trong file này đã đủ để bạn hiểu tốt hơn những abstraction phía trên.

19. Bài thực hành trên máy

Với JDK portable của bạn:

# compile
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javac.exe .\src\l01\Main.java

# xem class signatures
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe .\src\l01\Main.class

# xem bytecode
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe -c .\src\l01\Main.class

# xem cả Constant Pool + metadata
jdk-21_windows-x64_bin\jdk-21.0.12.1\bin\javap.exe -c -v .\src\l01\Main.class

Nên thử các ví dụ sau rồi so sánh bytecode:

String a = "abc";
String b = "abc";
String c = new String("abc");

int x = 10;
int y = 20;
int z = x + y;

Person p = new Person();

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

Tài liệu học cá nhân — Java/JVM fundamentals.