JVM 구조와 작동 원리 — Bytecode가 CPU에서 실행되기까지
Java Roadmap의 JVM 영역을 Zoom-in해 Class Loading, Linking/Initialization, Runtime Data Areas, Frame, Interpreter/JIT, GC와 Native 경계를 하나의 실행 흐름으로 연결한다.
Java 전체 지형에서 JVM은 Java 소스 코드와 실제 OS/CPU 실행 사이의 Runtime 계층에 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java Language
↓
.java Source
↓ javac
.class Bytecode
↓
┌──────── JVM ────────┐
│ Loading │
│ Linking │
│ Initialization │
│ Runtime Data Areas │
│ Execution │
│ GC / Native Bridge │
└─────────────────────┘
↓
Native Execution
↓
OS / CPU
이 글은 JVM 구성요소를 외우는 것이 아니라 .class가 들어온 뒤 무엇이 어떤 순서로 필요해지는가를 따라간다.
1. JVM은 무엇을 실행하는가
javac이 만드는 .class는 특정 CPU의 x86-64나 ARM64 기계어가 아니라 JVM Instruction Set을 사용하는 Bytecode와 실행 메타데이터를 담는 class file format이다.
1
2
3
4
5
6
7
.java
↓ javac
.class
├─ Bytecode
├─ Constant Pool
├─ Field / Method 정보
└─ 기타 Metadata
따라서 CPU가 .class를 직접 실행하는 것이 아니다.
1
2
3
4
5
6
7
.class
↓
JVM이 읽고 실행
↓
필요하면 Native Code로 변환
↓
CPU
이 지점이 네이티브 실행 모델과 Java 실행 모델이 갈라지는 핵심이다.
2. 실행하려면 먼저 Class를 JVM 안으로 가져와야 한다
JVM은 필요한 Class와 Interface를 동적으로 Loading → Linking → Initialization한다.
1
2
3
4
5
6
7
8
9
10
11
12
.class Binary
↓
Loading
↓
Linking
├─ Verification
├─ Preparation
└─ Resolution
↓
Initialization
↓
실행 가능한 JVM Runtime State
Loading
Class Loader가 Class의 Binary Representation을 찾아 JVM 내부의 Class/Interface 표현을 만든다.
여기서 중요한 점은 Class의 정체성이 이름만으로 결정되지 않는다는 것이다.
1
2
3
4
5
Class Identity
=
Fully Qualified Class Name
+
Defining Class Loader
따라서 이름이 같은 Class라도 서로 다른 Class Loader가 정의하면 JVM에서는 다른 Type이 될 수 있다.
Linking
Loading된 Class를 실제 실행 상태에 연결한다.
1
2
3
4
5
6
7
8
Verification
→ class file과 Bytecode가 JVM 제약을 만족하는지 검사
Preparation
→ static field 등을 위한 실행 준비
Resolution
→ Constant Pool의 Symbolic Reference를 실제 Class·Field·Method와 연결
Resolution은 구현과 실행 상황에 따라 필요한 시점까지 늦춰질 수 있다.
Initialization
Class 초기화 메서드 <clinit>를 실행해 static field initializer와 static block 같은 Class 초기화 로직을 수행한다.
즉 다음 셋은 같은 작업이 아니다.
1
2
3
4
5
6
7
8
Loading
→ Class를 JVM 안에 가져온다
Linking
→ 실행할 수 있도록 검증·준비·참조 연결
Initialization
→ Class의 static 초기화 코드를 실행
3. 가져온 Class와 실행 상태는 어디에 존재하는가
이제 JVM 내부의 Runtime Data Areas로 Zoom-in한다.
1
2
3
4
5
6
7
8
9
10
11
12
JVM Runtime Data Areas
JVM 전체에서 공유
├─ Heap
└─ Method Area
└─ Run-Time Constant Pool
Thread마다 존재
├─ pc Register
├─ JVM Stack
│ └─ Frame
└─ Native Method Stack
이 구분에서 먼저 기억할 것은 공유 영역과 Thread별 영역이다.
Heap
Object와 Array가 할당되는 공유 Runtime 영역이다.
1
2
3
4
5
new Member()
↓
Heap
↓
Object
Heap은 모든 JVM Thread가 공유하며 Garbage Collection의 주요 대상이다.
Method Area
Class별 구조, Method/Field 정보, Method Code 등 JVM이 Class를 실행하는 데 필요한 데이터를 보관하는 논리적 Runtime 영역이다.
HotSpot의 구체 구현인 Metaspace와 JVMS의 Method Area라는 추상적 개념을 그대로 같은 말로 보지 않는다.
JVM Stack과 Frame
Method가 호출될 때 Thread의 JVM Stack에는 Frame이 만들어진다.
1
2
3
4
5
6
Thread
↓
JVM Stack
├─ Frame: methodA
├─ Frame: methodB
└─ Frame: methodC ← 현재 실행
Frame의 핵심은:
1
2
3
4
Frame
├─ Local Variables
├─ Operand Stack
└─ 현재 Method 실행에 필요한 상태
Bytecode는 Register 중심 CPU Instruction과 달리 Operand Stack을 적극적으로 사용하는 Stack Machine 모델을 가진다.
예를 들어 개념적으로:
1
2
3
4
5
6
7
값 push
값 push
iadd
↓
두 값을 Operand Stack에서 꺼내 더함
↓
결과를 다시 Stack에 push
처럼 실행된다.
4. Bytecode는 누가 실제로 실행하는가
여기서 Execution Engine이라는 구현 관점으로 넘어간다.
1
2
3
4
5
6
7
8
9
Bytecode
↓
JVM Implementation
├─ Interpreter
└─ JIT Compiler
↓
Native Code
↓
CPU
중요한 경계가 있다.
JVMS는 JVM이 지켜야 하는 추상 실행 모델을 정의하지만, Interpreter/JIT의 구체 전략은 JVM 구현체가 선택한다.
HotSpot 같은 JVM 구현체는 Bytecode를 실행하면서 반복적으로 실행되는 Code를 Native Code로 Compile해 이후 실행 비용을 줄일 수 있다.
따라서 Java를 단순히:
1
Java = Interpreter Language
라고 보는 것은 부정확하다.
더 정확한 그림은:
1
2
3
4
5
6
7
8
9
10
11
Bytecode
↓
Interpreter로 실행 가능
↓
Runtime Profile 수집
↓
Hot Code
↓ JIT
Native Code
↓
CPU 직접 실행
이다.
5. GC는 JVM 실행 모델의 어디에 있는가
Java Code는 일반적으로 Object의 물리 메모리 해제를 직접 호출하지 않는다.
1
2
3
4
5
6
7
8
9
Object 생성
↓
Heap
↓
Reachability 변화
↓
더 이상 도달할 수 없는 Object
↓
GC가 회수 가능한 대상으로 판단
하지만 GC Algorithm 자체는 JVM Specification이 하나로 고정하지 않는다.
즉:
1
2
3
4
5
JVM Specification
→ Heap과 자동 Storage Management의 계약
JVM Implementation
→ 실제 GC Algorithm과 Heap Layout 선택
으로 구분한다.
G1, ZGC 같은 구체 Collector는 이 문서의 다음 세부가 아니라 GC 자체가 필요할 때 별도로 Zoom-in할 영역이다.
6. Native Code와 OS 경계는 사라지지 않는다
JVM 안에서 실행한다고 OS와 CPU가 없어지는 것은 아니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java Application
↓
JVM
├─ Bytecode Execution
├─ JIT
├─ GC
└─ Runtime Services
↓
Native Code / Native Library
↓
OS System Call
↓
Kernel
↓
CPU / Hardware
Native Method가 필요하면 JVM은 Native Implementation과 연결할 수 있다. JVM 자체도 결국 현재 OS와 CPU 위에서 실행되는 Native Program이다.
따라서 Java의 Platform Independence는:
1
2
3
같은 .class를
각 Platform용 JVM이 받아
그 Platform에서 실행한다
는 의미이지 CPU와 OS가 사라진다는 뜻이 아니다.
7. 처음부터 끝까지 다시 연결한다
이제 세부를 다시 전체 실행 흐름에 놓는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.java Source
↓ javac
.class
↓
Class Loading
↓
Linking
├─ Verification
├─ Preparation
└─ Resolution
↓
Initialization
↓
Runtime Data Areas에 실행 상태 형성
↓
Method 호출
↓
Thread의 JVM Stack에 Frame 생성
↓
Bytecode 실행
↓
Interpreter / JIT
↓
Native Code
↓
OS / CPU
그리고 실행 중에는 옆에서:
1
2
3
4
5
Heap
↕
Object Allocation
↕
Garbage Collection
이 함께 움직인다.
8. 다시 Java 전체 지형으로 Zoom-out
이 문서에서 본 것은 Java 전체가 아니라 JVM / Runtime 영역 하나다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java
│
├─ Language
│
├─ JVM / Runtime ← 지금 Zoom-in한 위치
│ ├─ Class Loading
│ ├─ Runtime Data Areas
│ ├─ Frame / Bytecode
│ ├─ Interpreter / JIT
│ └─ GC
│
├─ Standard Library
│
├─ Concurrency / JMM
│
└─ Ecosystem
├─ Gradle
└─ JPA / Hibernate
다음에 더 내려갈 수 있는 지점은 서로 다르다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class Loading이 궁금하다
→ Class Loader / Delegation
메모리가 궁금하다
→ Heap / Stack / Metaspace / GC
성능이 궁금하다
→ Interpreter / JIT / Code Cache
동시성이 궁금하다
→ Java Memory Model / happens-before
Native 경계가 궁금하다
→ JNI / Native Library
세부를 공부한 뒤에는 다시 Java Roadmap으로 돌아가 현재 지식의 위치를 재확인한다.
JVM은 Bytecode를 단순히 한 줄씩 해석하는 프로그램이 아니다. Class를 동적으로 Loading·Linking·Initialization하고, Thread별 실행 상태와 공유 Runtime 영역을 관리하며, 구현체에 따라 Interpreter·JIT·GC 같은 실행 서비스를 제공하는 Java의 Runtime 계층이다.