F-LAB JAVA · 2주차 · Phase 2 · JVM 메서드 실행 메커니즘
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
두 줄의 자바 코드가 JVM에서는 5개의 큰 단계로 펼쳐진다.
Phase 1 ~ Unit 2.2에서 본 모든 메커니즘이 시간순으로 한 자리에 모이는 순간이다.
그리고 두 번째 호출부터는 JIT가 이 비용을 거의 0으로 만든다.
public class App {
public static void main(String[] args) {
ShipmentCalculator calc = new ShipmentCalculator(); // ← 한 줄
BigDecimal fare = calc.calculate(100, 50); // ← 또 한 줄
}
}
public class ShipmentCalculator {
private int callCount;
public BigDecimal calculate(int weight, int ratePerKg) {
callCount++;
return BigDecimal.valueOf((long) weight * ratePerKg);
}
}
이 두 줄의 코드가 JVM에서 펼쳐지는 모습 전체.
1. 5단계 큰 그림 — 한 페이지 요약
2. Stage 1 — JVM 시작과 클래스 로딩
3. Stage 2 — main() 실행 시작
4. Stage 3 — new로 객체 생성
5. Stage 4 — 참조 저장
6. Stage 5 — 인스턴스 메서드 호출
7. JIT의 마법 — 두 번째 호출부터의 변화
8. ILIC 실무 — Hot path 분석과 워밍업
9. 면접 질문 + 자기 점검
시간 →
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: JVM 시작 + 클래스 로딩 │
│ App.class, ShipmentCalculator.class 읽기 │
│ Method Area에 적재 │
├─────────────────────────────────────────────────────────────┤
│ Stage 2: main() 실행 시작 │
│ App의 Static Zone에서 main 바이트코드 발견 │
│ 첫 스택 프레임 생성 │
├─────────────────────────────────────────────────────────────┤
│ Stage 3: new ShipmentCalculator() │
│ Heap에 객체 할당 │
│ Object Header 설정 (Class Pointer) │
│ 생성자 호출 (invokespecial) │
├─────────────────────────────────────────────────────────────┤
│ Stage 4: 객체 참조를 calc 변수에 저장 │
│ astore 명령으로 LVA에 저장 │
├─────────────────────────────────────────────────────────────┤
│ Stage 5: calc.calculate(100, 50) │
│ Method Resolution (1단계) │
│ Method Invocation (2단계) │
│ 바이트코드 실행 │
└─────────────────────────────────────────────────────────────┘
| Stage | 비용 | 빈도 |
|---|---|---|
| 1. 클래스 로딩 | 매우 큼 (수 ms ~ 수십 ms) | 클래스당 1번 |
| 2. main 시작 | 작음 | 앱 시작 1번 |
| 3. 객체 생성 | 보통 (수 μs) | new마다 |
| 4. 참조 저장 | 매우 작음 (ns) | 매번 |
| 5. 메서드 호출 | 보통 → JIT 후 매우 작음 | 매번 |
가장 비싼 단계 = Stage 1 (클래스 로딩).
하지만 클래스당 단 1번이라 amortize 됨.
→ 실시간 비용 관점에선 Stage 3 (객체 생성) 가 가장 자주 신경 써야 할 부분.
첫 번째 호출 시 비용:
Stage 1: 클래스 로딩 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Stage 2: main 시작 ▓
Stage 3: new ▓▓▓
Stage 4: 저장 ·
Stage 5: 메서드 호출 ▓▓
두 번째 호출 시 비용:
Stage 1: (이미 로딩됨) ·
Stage 2: (이미 시작) ·
Stage 3: new ▓▓▓
Stage 4: 저장 ·
Stage 5: 메서드 호출 ▓▓ → JIT 후 ·
→ 첫 호출은 매우 비싸지만, 이후 호출은 거의 무료. JIT의 효과.
java App
이 한 줄로 일어나는 일:
1. java 명령 → JVM 프로세스 시작
2. JVM이 자기 자신 초기화
- 메모리 영역 할당 (Heap, Method Area, Stack 등)
- 기본 클래스 로더 준비
3. 시스템 클래스 로딩 (java.lang.Object, String, System 등)
4. App.class 찾기 (-classpath의 경로 검색)
5. App 클래스 로딩 시작
→ java App 실행 후 main()이 실제로 호출되기까지 100~200ms.
→ Spring Boot 앱은 클래스 수천 개 로딩으로 수 초 걸림.
JVM이 App.class를 만나면:
┌─────────────────────────────────────────────┐
│ 1. Loading │
│ - 파일 시스템에서 .class 바이트 읽기 │
│ - 바이트를 Method Area에 적재 │
│ - Class 객체를 Heap에 생성 │
├─────────────────────────────────────────────┤
│ 2. Linking │
│ - Verification: 바이트코드 검증 │
│ (스택 오버플로, 잘못된 타입 변환 등) │
│ - Preparation: static 변수 메모리 할당 │
│ (기본값으로 초기화) │
│ - Resolution: 심볼 참조 해소 (lazy) │
├─────────────────────────────────────────────┤
│ 3. Initialization │
│ - static 변수 초기값 할당 │
│ - static {} 블록 실행 │
└─────────────────────────────────────────────┘
코드:
public static void main(String[] args) {
ShipmentCalculator calc = new ShipmentCalculator();
...
}
JVM의 lazy loading:
java App 시작 시 즉시 로딩new ShipmentCalculator() 만나기 직전에 로딩Stage 1 (App 시작):
Method Area: App 클래스만
Stage 3 (new 만나기 직전):
Method Area: App + ShipmentCalculator
→ 호출되지 않는 클래스는 절대 로딩 안 됨. JVM의 메모리 절약 전략.
Method Area:
┌──────────────────────────────────────┐
│ App 클래스 │
│ Class Metadata Zone │
│ • 부모: Object │
│ • 메서드 시그니처: main(String[])V │
│ • main → static │
│ Static Zone │
│ • main() 바이트코드 │
│ Non-Static Zone │
│ • <init>() 바이트코드 (기본 생성자) │
└──────────────────────────────────────┘
Heap:
Class 객체들 (App.class, Object.class, String.class, ...)
Stack: (비어있음)
→ ShipmentCalculator는 아직 메모리에 없음.
# 클래스 로딩 추적
java -verbose:class App
# 출력 예:
# [Loaded java.lang.Object from ...]
# [Loaded App from file:./]
# [Loaded ShipmentCalculator from file:./] ← new 만나기 직전
운영에서 어떤 클래스가 언제 로딩되는지 확인 가능.
JVM:
1. App 클래스에서 main 메서드 검색
조건:
- public
- static
- void (또는 int — 일부 OS)
- String[] 매개변수
2. App의 Method Table에서 일치 시그니처 발견:
"main([Ljava/lang/String;)V"
3. Static Zone에서 main 바이트코드 위치 확정
1. main 메서드의 Method 구조체 조회:
- LVA 크기: 2 (args + 지역변수 calc 정도)
- Operand Stack 최대 크기: 2
2. JVM이 메인 스레드 생성:
- Stack Area 할당 (기본 512KB ~ 1MB)
- PC Register 준비
3. 첫 프레임 push:
main() 프레임
LVA[0] args = String[] (커맨드라인 인자)
LVA[1] (calc 자리, 아직 비어있음)
4. PC를 main의 첫 명령으로
Method Area:
App 클래스 (그대로)
Heap:
Class 객체들 + String[] 인자 (args)
Stack (메인 스레드):
┌──────────────────────────┐
│ main() 프레임 │ ← 첫 프레임
│ LVA[0] args = ref │
│ LVA[1] calc = ? │
│ Operand Stack: [] │
└──────────────────────────┘
PC: main의 첫 명령
public static void main(String[] args) {
ShipmentCalculator calc = new ShipmentCalculator();
BigDecimal fare = calc.calculate(100, 50);
}
바이트코드 (대략):
0: new #2 // ShipmentCalculator
3: dup
4: invokespecial #3 // <init>
7: astore_1 // calc에 저장
8: aload_1 // calc 로드
9: bipush 100
11: bipush 50
13: invokevirtual #4 // calculate(II)Ljava/math/BigDecimal;
16: astore_2 // fare에 저장
17: return
PC=0부터 시작. 이제 명령 하나씩 따라가며 Stage 3~5 진행.
ShipmentCalculator calc = new ShipmentCalculator();
0: new #2 // ShipmentCalculator
3: dup
4: invokespecial #3 // <init>
7: astore_1 // calc에 저장
→ Unit 1.5에서 본 패턴. 이번엔 시간 흐름에 따라 정밀 추적.
new #2 — 메모리 할당JVM 동작:
1. 상수 풀의 #2 항목 조회
→ ShipmentCalculator 클래스 심볼
2. ShipmentCalculator 클래스 로딩 확인
→ 아직 로딩 안 됨!
→ Stage 1 클래스 로딩 절차 실행 (Loading → Linking → Initialization)
→ Method Area에 ShipmentCalculator 클래스 적재
3. 객체 크기 계산
- Header: 16 bytes
- Instance field (callCount: int): 4 bytes
- Padding: 4 bytes
- 총: 24 bytes
4. Heap의 Eden Space에서 메모리 확보
- TLAB 활용 (메인 스레드의 TLAB)
5. 0으로 초기화 (callCount = 0)
6. Object Header 설정
- Mark Word: 초기 상태
- Class Pointer: ShipmentCalculator Klass 주소
7. 객체 참조를 Operand Stack에 push
이 순간의 메모리:
Method Area:
┌──────────────────────────────────┐
│ ShipmentCalculator 클래스 │ ← 방금 로딩됨
│ Method Table: │
│ <init>()V │
│ calculate(II)BigDecimal │
│ Non-Static Zone: │
│ <init> 바이트코드 │
│ calculate 바이트코드 │
└──────────────────────────────────┘
Heap:
┌──────────────────────────┐
│ Calculator @0x7f4a2c01 │ ← 방금 할당됨
│ Mark Word │
│ Class ptr → Calc Klass │
│ callCount = 0 │ ← 미초기화 상태 (생성자 호출 전)
└──────────────────────────┘
Stack:
main() 프레임
LVA[0] args
LVA[1] calc = ?
Operand Stack: [0x7f4a2c01] ← new의 결과
dup — Stack top 복제0 : new # Operand Stack: [0x7f4a2c01]
3 : dup # Operand Stack: [0x7f4a2c01, 0x7f4a2c01]
왜 복제?
invokespecial이 객체 참조를 소비함 (this로 사용)astore_1에서 calc에 저장해야 하니까 또 하나 필요→ Unit 1.5 6장에서 본 패턴. 객체 생성의 표준.
invokespecial #3 — 생성자 호출4: invokespecial #3 // <init>()V
ShipmentCalculator에 명시적 생성자 없음 → JVM이 기본 생성자 자동 생성.
public class ShipmentCalculator {
private int callCount;
// 컴파일러가 추가하는 기본 생성자
public ShipmentCalculator() {
super(); // Object 생성자 호출
}
}
호출 흐름:
1. Operand Stack top의 객체 참조를 인자로
Stack: [0x7f4a2c01, 0x7f4a2c01] → 1개 소비
2. 새 스택 프레임 생성:
<init>() 프레임
LVA[0] this = 0x7f4a2c01
3. 생성자 실행:
- super() 호출 → Object의 <init>() 호출 → 또 새 프레임 (재귀)
- callCount는 이미 0이라 생략
4. 생성자 종료:
- <init> 프레임 pop
- return (void)
이 순간 메모리:
Stack:
┌──────────────────────────┐
│ main() 프레임 │
│ Operand Stack: │
│ [0x7f4a2c01] │ ← invokespecial 후 1개 남음
└──────────────────────────┘
Heap:
Calculator @0x7f4a2c01
callCount = 0 ← 생성자 끝, 정상 객체
astore_1 — 참조 저장 (Stage 4로 연결)7: astore_1
Operand Stack의 top을 LVA[1]에 저장.
Stack:
main() 프레임
LVA[0] args
LVA[1] calc = 0x7f4a2c01 ← 저장됨
Operand Stack: []
이제 calc 변수가 객체를 가리킴 → Stage 4 완료.
→ 첫 객체 생성이 가장 비싸다.
이미 Stage 3 마지막에서 astore_1로 calc에 저장 완료.
이 단계는 단순하지만 변수의 역할 변화를 명확히 하기 위해 분리.
Heap:
Calculator @0x7f4a2c01
Stack:
main() 프레임
LVA[1] calc = 0x7f4a2c01 ← 객체에 대한 "이름"
calc는 Stack에 있는 참조 변수BigDecimal fare = calc.calculate(100, 50);
바이트코드:
8: aload_1 // calc 로드 (LVA[1] → Operand Stack)
9: bipush 100 // 100 push
11: bipush 50 // 50 push
13: invokevirtual #4 // calculate(II)BigDecimal
16: astore_2 // fare에 저장
→ Stage 5에서 본격 추적.
aload_1, bipush8 : aload_1 // calc 로드
9 : bipush 100 // 100
11: bipush 50 // 50
이 명령 후의 Operand Stack:
[0x7f4a2c01, 100, 50]
↑ ↑ ↑
this 인자 arg arg
→ 첫 번째 인자가 객체 참조(this).
→ Unit 2.2 5장에서 본 this 전달의 메커니즘.
13: invokevirtual #4
JVM 동작:
1. 상수 풀의 #4 항목 조회
→ "ShipmentCalculator.calculate(II)Ljava/math/BigDecimal;"
2. Operand Stack top에서 N개 인자 + 1개 this 위치 파악
- 시그니처 (II) → 2 슬롯 (int 2개)
- this까지 합해서 3 슬롯
- 즉, Stack의 top 3개
3. Stack top - 2 위치의 객체(this) 의 Class Pointer 확인
→ ShipmentCalculator Klass
4. ShipmentCalculator Klass의 VMT에서 calculate(II) 슬롯 찾기
→ 해당 슬롯이 가리키는 Method 구조체 획득
5. 접근 권한 검증 (public)
6. 메서드 실행 준비 완료
→ 첫 호출은 위 1~6 모두 수행 (느림).
→ 두 번째 호출은 인라인 캐시 활용 (7장에서).
1. Method 구조체에서 메서드 정보:
- LVA 크기: 3 슬롯 (this, weight, ratePerKg)
- Operand Stack 최대: 4
2. 새 스택 프레임 생성:
calculate() 프레임
LVA[0] this = 0x7f4a2c01 ← Operand Stack에서 가져옴
LVA[1] weight = 100
LVA[2] ratePerKg = 50
Operand Stack: []
3. main 프레임의 Operand Stack에서 3개 인자 제거
main의 Stack: []
4. Return Address 저장:
calculate 프레임의 Frame Data에 main의 다음 PC(=16) 기록
5. PC를 calculate의 첫 명령으로 점프
메모리:
Stack:
┌──────────────────────────────────┐
│ calculate() 프레임 │ ← 새 프레임
│ LVA[0] this = 0x7f4a2c01 │
│ LVA[1] weight = 100 │
│ LVA[2] ratePerKg = 50 │
│ Operand Stack: [] │
│ Return Address → main PC=16 │
├──────────────────────────────────┤
│ main() 프레임 │
│ LVA[0] args │
│ LVA[1] calc = 0x7f4a2c01 │
│ LVA[2] fare = ? │
│ Operand Stack: [] │
└──────────────────────────────────┘
Heap:
Calculator @0x7f4a2c01
callCount = 0
public BigDecimal calculate(int weight, int ratePerKg) {
callCount++;
return BigDecimal.valueOf((long) weight * ratePerKg);
}
바이트코드:
0: aload_0 // this 로드
1: dup // 복제
2: getfield #5 // callCount 읽기
5: iconst_1 // 1 push
6: iadd // callCount + 1
7: putfield #5 // this.callCount = 결과
10: iload_1 // weight
11: i2l // int → long 변환
12: iload_2 // ratePerKg
13: i2l
14: lmul // 곱셈
15: invokestatic #6 // BigDecimal.valueOf(long)
18: areturn // 결과 반환
PC=0: aload_0
Stack: [this]
PC=1: dup
Stack: [this, this]
PC=2: getfield #5 // callCount
Stack: [this, 0] ← top의 this에서 callCount 읽음
PC=5: iconst_1
Stack: [this, 0, 1]
PC=6: iadd
Stack: [this, 1] ← 0 + 1 = 1
PC=7: putfield #5 // this.callCount = 1
Stack: []
Heap의 this 객체:
callCount = 1 ← 변경됨!
PC=10: iload_1 // weight
Stack: [100]
PC=11: i2l // int → long
Stack: [100L]
PC=12: iload_2 // ratePerKg
Stack: [100L, 50]
PC=13: i2l
Stack: [100L, 50L]
PC=14: lmul // 곱셈
Stack: [5000L]
PC=15: invokestatic BigDecimal.valueOf(long)
→ 또 다른 메서드 호출. 새 프레임 push, BigDecimal 클래스의 valueOf 실행.
→ 이 호출도 내부적으로 Stage 5 전체를 다시 거침 (재귀적!).
BigDecimal.valueOf 종료 후:
Stack: [BigDecimal_ref] ← 5000을 표현하는 BigDecimal 객체 참조
PC=18: areturn
- Stack top의 참조를 호출자에게 반환
- calculate 프레임 pop
- PC를 Return Address(=16)로 복원
main() 프레임 (복귀 후):
LVA[0] args
LVA[1] calc = 0x7f4a2c01
LVA[2] fare = ?
Operand Stack: [BigDecimal_ref] ← calculate가 반환한 값
PC = 16
PC=16: astore_2 // fare에 저장
Stack: []
LVA[2] fare = BigDecimal_ref
이제 fare 변수가 BigDecimal 객체를 가리킴.
시간 →
[1] Resolution: 클래스/메서드 찾기 (≈ 1μs)
[2] Invocation: 새 프레임 생성, this+인자 전달 (≈ 100ns)
[3] 바이트코드 실행 (callCount++, 곱셈, BigDecimal) (≈ 1μs)
[4] 메서드 반환 (≈ 50ns)
총 약 2-3 마이크로초. 그러나 이는 첫 호출만.
같은 객체의 메서드를 100번 호출하면 매번 metadata 조회를 하는가?
답: 처음에만. 이후엔 거의 안 함. JIT 덕분.
JVM의 메서드 실행 흐름:
첫 호출 ~ 10번째 호출:
Interpreter 모드
바이트코드를 한 줄씩 해석 (위 Stage 5 그대로)
10번째 ~ 10000번째:
C1 (Client) 컴파일러 작동
바이트코드 → 네이티브 코드 (간단한 최적화)
실행 속도 ~10배 향상
10000번째 이후:
C2 (Server) 컴파일러 작동
공격적 최적화 (인라이닝, 디버추얼라이제이션)
실행 속도 ~100배 향상
for (int i = 0; i < 1_000_000; i++) {
calc.calculate(100, 50); // 100만 번 호출
}
첫 호출:
- Stack의 객체 → Class Pointer → ShipmentCalculator Klass
- VMT에서 calculate 슬롯 찾기
- Method 구조체 획득
두 번째 호출부터 (인라인 캐시):
호출 사이트(call site)에 캐시:
"여기서 호출된 Klass = ShipmentCalculator"
"그때의 Method 구조체 = 캐시된 포인터"
다음 호출:
- 현재 객체의 Klass == 캐시된 Klass? (포인터 비교)
- 같으면 → 캐시된 Method 구조체 바로 사용
- 다르면 → 일반 Resolution (다형성 케이스)
→ Resolution 비용 거의 0.
C2 컴파일러가 보고 판단:
"이 호출 사이트는 99% ShipmentCalculator를 호출한다"
"calculate 메서드는 작다 (인라이닝하기 적합)"
→ 호출 자체를 없애고 calculate 코드를 호출자에 펼침
결과:
// 원본 코드
for (int i = 0; i < 1_000_000; i++) {
calc.calculate(100, 50);
}
// JIT 최적화 후 (개념적, 실제는 네이티브 코드)
for (int i = 0; i < 1_000_000; i++) {
calc.callCount++;
BigDecimal.valueOf(100L * 50L);
// ↑ 이것도 인라이닝되거나 캐시
}
// 더 적극적 최적화:
calc.callCount = 1_000_000; // 루프 자체 제거
// 결과 사용 안 되면 BigDecimal.valueOf 호출도 제거
→ 메서드 호출이 사라지고, 단순 산술로 변환.
마이크로벤치마크:
@Benchmark
public BigDecimal callOnce() {
return calc.calculate(100, 50);
}
JMH 측정 결과 (가상):
Mode: AverageTime
Cold (첫 호출): ≈ 2 - 3 μs
Warm (인터프리터): ≈ 200 - 500 ns
Hot (C1 컴파일): ≈ 20 - 50 ns
Burning (C2 컴파일): ≈ 1 - 5 ns
→ 첫 호출과 최적화 후 호출은 1000배 차이.
@Benchmark
public BigDecimal benchmark() {
return calc.calculate(100, 50);
}
JMH 같은 벤치마크 도구가 워밍업 라운드를 두는 이유:
→ "이 코드 느린데?" 의심 전에 워밍업 후 측정.
# 어떤 메서드가 JIT 컴파일됐는지 보기
java -XX:+PrintCompilation App
# 출력 예:
# 1 n 0 java.lang.System::arraycopy (native)
# 2 b 3 java.lang.String::hashCode (44 bytes)
# ...
# 123 b 4 ShipmentCalculator::calculate (28 bytes)
# ↑ 이 메서드가 C2 컴파일됨
# 인라이닝 추적
java -XX:+PrintInlining -XX:+UnlockDiagnosticVMOptions App
운영에서 hot path 확인 시 위 옵션 사용.
@RestController
public class ShipmentController {
private final ShipmentService service;
@GetMapping("/{id}")
public ShipmentResponse get(@PathVariable Long id) {
return service.findById(id);
}
}
배포 직후:
첫 요청: 200~500ms
- 클래스 로딩 다수
- 메서드 인터프리터 모드
- JPA Proxy 생성
100번째 요청: 50~100ms
- 클래스 로딩 완료
- C1 컴파일 시작
1000번째 요청: 10~20ms
- C2 컴파일 적용
- 인라이닝 효과
→ 배포 직후 성능이 안 좋다고 놀라지 말 것. 자연스러운 워밍업 구간.
Blue-Green / Canary 배포:
1. 새 인스턴스(Green) 배포
2. 트래픽 5% → 모니터링 → 워밍업
3. 100ms 미만 안정화 후 트래픽 50% → 100%
4. 구 인스턴스(Blue) 종료
→ JIT 워밍업을 고려한 표준 배포 전략.
GraalVM Native Image:
native-image -jar app.jar
./app
특징:
단점:
→ Spring Boot 3.x + Spring Native로 도입 시도 가능.
→ 서버리스, CLI 도구에 적합.
Async-profiler 사용:
# 30초간 CPU 프로파일링
async-profiler -d 30 -f profile.html <PID>
출력 (Flame Graph):
ShipmentController.get() ████████████████████░ 85%
ShipmentService.findById() ████████████░ 50%
ShipmentRepository.findById() ███████░ 35%
Hibernate query ███████ 32%
JDBC ██████ 25%
→ 어느 메서드에서 CPU 시간이 소비되는지 시각화.
→ 운영 최적화의 출발점.
ILIC 시나리오:
log.info) 이 시간 잡아먹음log.info("..." + bigData) 의 String concatlog.info("...{}", bigData) 로 변경→ 추측 대신 측정. 의외의 곳에서 비용 발생.
// 잘못된 패턴
for (Cargo c : cargoes) {
if (c.getStatus().equals(ACTIVE)) { // ← getStatus() 호출 매번
for (Item i : c.getItems()) { // ← getItems() 호출 매번
// ...
}
}
}
// 개선된 패턴
for (Cargo c : cargoes) {
Status status = c.getStatus();
List<Item> items = c.getItems();
if (status.equals(ACTIVE)) {
for (Item i : items) {
// ...
}
}
}
→ JIT가 어차피 최적화하지만, 명시적으로 변수에 저장하면 더 안전.
| Q | 핵심 답변 |
|---|---|
new Calc()의 4가지 작업? | 클래스 확인 → 메모리 할당 → Header 설정 → 생성자 호출 |
dup 명령의 필요성? | new 후 객체 참조를 invokespecial이 소비. astore도 써야 해서 미리 복제 |
| 클래스 로딩은 언제? | 처음 참조될 때 (lazy). new, 정적 호출, Class.forName 등이 트리거 |
| 5단계 중 가장 비싼 단계? | 클래스 로딩. 하지만 클래스당 1번이라 amortize |
| 같은 메서드 100번 호출 시 매번 Resolution? | 첫 번째만. 이후 인라인 캐시 활용 |
| JIT 컴파일러 단계? | Interpreter → C1 (≥10 호출) → C2 (≥10K 호출) |
| 첫 호출과 hot 호출 성능 차이? | 1000배까지 가능. JIT의 인라이닝 효과 |
| 워밍업이 필요한 이유? | JIT가 최적화하기까지 시간 필요. 배포 직후 느림 |
| Native Image의 장점? | 시작 시간 수십ms, 메모리 1/5. 단 빌드 길고 제약 있음 |
| Hot path 어떻게 찾나? | 프로파일러 (Async-profiler, JFR). Flame graph 시각화 |
new 한 줄의 4가지 바이트코드를 그릴 수 있다dup이 필요한 이유를 설명할 수 있다Model2 m2 = new Model2(); m2.hap(1, 2); 의 전 과정1. 두 줄의 코드 = 5단계의 거대한 작업
2. JIT가 비용을 1000배 줄인다
3. ILIC 실무 직결
이번 Unit에서 메서드 호출의 전 과정을 봤다면, 다음은 new에 집중해서 4가지 작업을 정밀 추적.
→ Phase 2의 마지막 Unit.
→ 그 다음 Phase 3 (정점).
✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
🚀 Phase 2 — JVM 메서드 실행 메커니즘
✅ Unit 2.1 메서드 호출의 2단계 처리
✅ Unit 2.2 Static vs 인스턴스 호출 경로
✅ Unit 2.3 인스턴스 메서드 호출의 전 과정 ← 여기
⏭ Unit 2.4 new 연산자의 실제 동작
⏭ Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점