F-LAB JAVA · 2주차 · Phase 2 · JVM 메서드 실행 메커니즘
🚀 Phase 2 시작 — Phase 1의 메모리 구조 위에서 메서드 호출 추적
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
s.calculate() 한 줄이 JVM에서 정확히 어떤 순서로 처리되나?모든 메서드 호출은 "무엇을 호출하는가"(시그니처 확인) 와 "그 실체는 어디에 있는가"(바이트코드 실행) 의 2단계로 분리된다.
Class Metadata Zone에서 시그니처를 확인하고, Static/Non-Static Zone에서 실제 바이트코드를 실행한다.
이 분리가 다형성·AOP·동적 프록시를 모두 가능하게 만드는 근본 메커니즘이다.
| 단계 | 식당 비유 | JVM |
|---|---|---|
| 1단계 | 손님이 "스파게티 하나" 라고 외침 | "calculate() 호출하자" — 시그니처 |
| 2단계 | 주방에서 "스파게티 레시피" 찾아 조리 | 실제 바이트코드 실행 |
1. 우리가 보는 한 줄 vs JVM이 하는 작업
2. 1단계 — Method Resolution (시그니처 확인)
3. 2단계 — Method Invocation (바이트코드 실행)
4. Static Dispatch vs Dynamic Dispatch
5. 메서드 영역 — 시그니처와 바이트코드 매핑
6. 다형성·오버라이딩이 의존하는 2단계
7. ILIC 실무 — Spring AOP · JPA Proxy의 비밀
8. 흔한 실수 + 성능 이슈
9. 면접 질문 + 자기 점검
Shipment s = repository.findById(1L).orElseThrow();
BigDecimal fare = s.calculate(100); // ← 이 한 줄
우리가 보기에:
"s 객체의 calculate 메서드를 100 인자로 호출"
JVM이 실제로 하는 일은 훨씬 많다.
"s.calculate(100)" 한 줄을 만났을 때:
🔍 Phase A: Method Resolution (메서드 결정)
1. s의 참조 따라가서 Heap의 객체 찾기
2. 객체의 Class Pointer → Shipment Klass
3. Klass의 Class Metadata Zone에서 "calculate(int)" 시그니처 찾기
4. 메서드 접근 권한 검증 (public? private?)
5. 메서드 호출 방식 결정 (virtual? special? static?)
🚀 Phase B: Method Invocation (메서드 실행)
1. 해당 메서드의 바이트코드 위치 찾기 (Static or Non-Static Zone)
2. 새 스택 프레임 생성 (LVA 크기 결정, Operand Stack 초기화)
3. 매개변수와 this 전달
4. PC를 메서드 첫 명령으로 이동
5. 바이트코드 실행 시작
→ "무엇을 부를지" 와 "어떻게 부를지" 는 다른 단계.
Cargo c = getCargo(); // DryCargo? ReeferCargo? HazmatCargo?
c.calculateFee(); // 어느 메서드 호출?
만약 1단계만으로 끝나면 컴파일 시점에 모든 게 결정돼야 함.
2단계 구조 덕에 1단계(시그니처)는 컴파일 타임에, 2단계(실제 메서드)는 런타임에 결정 가능.
@Transactional
public Shipment save(Shipment s) { ... }
Spring은 이 메서드 호출을 가로채서 @Transactional 동작을 추가.
어떻게? → 1단계(시그니처)는 그대로 두고 2단계(실행)를 프록시 객체로 바꿔치기.
// 컴파일 시점:
SomeClass.someMethod(); // ← 호출만 컴파일
// 런타임 시점:
// 1단계에서 SomeClass가 처음 로딩됨
// 2단계에서 실제 메서드 바이트코드 실행
→ 호출되지 않는 클래스는 메모리에 로딩 안 됨. JVM의 lazy loading 전략.
Unit 1.3에서 본 Class Metadata Zone 의 내용 중:
Shipment 클래스의 Metadata:
...
├─ 메서드 시그니처 목록:
│ ├─ calculate(int) : BigDecimal (public)
│ ├─ setBlNo(String) : void (public)
│ ├─ validate() : void (private)
│ ├─ getId() : Long (public)
│ └─ ...
└─ 상수 풀 (Constant Pool)
이 시그니처 목록에서 호출하려는 메서드를 찾는 작업이 1단계.
자바에서 메서드 시그니처 = 이름 + 매개변수 타입 (순서대로).
반환 타입은 시그니처에 포함 안 됨 (오버로딩 규칙).
public BigDecimal calculate(int weight) // calculate(I)
public BigDecimal calculate(int weight, BigDecimal rate) // calculate(I, BigDecimal)
public Integer calculate(int weight) // ❌ 시그니처 충돌 (반환 타입만 다름)
JVM 내부 표기:
calculate(I)Ljava/math/BigDecimal;
↑ ↑
매개변수 디스크립터 반환 디스크립터
I = int
Ljava/math/BigDecimal; = BigDecimal 객체
V = void
→ 1단계에서 JVM은 이 디스크립터로 정확한 메서드를 식별.
1단계 — Method Resolution 흐름:
┌─────────────────────────────────────────┐
│ 1. 호출 사이트(call site)의 상수 풀 인덱스 │
│ 예: invokevirtual #15 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 2. 상수 풀의 #15 항목 = MethodRef │
│ → "Shipment.calculate(I)BigDecimal" │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 3. Shipment 클래스의 Method Table 검색 │
│ → calculate(I) 발견 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 4. 접근 권한 / abstract 여부 검증 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 5. 메서드 항목(Method 구조체) 반환 │
│ → 바이트코드 위치 포함 │
└─────────────────────────────────────────┘
→ 상수 풀의 인덱스 → 실제 메서드 객체 로 변환되는 과정.
→ Phase 3에서 상수 풀과 함께 더 깊이 본다.
해당 클래스에 메서드가 없으면 부모 체인을 따라 탐색.
class BaseEntity {
public void touch() { lastModifiedAt = LocalDateTime.now(); }
}
class Shipment extends BaseEntity {
// touch() 안 정의
}
Shipment s = new Shipment();
s.touch(); // 호출됨
Resolution 흐름:
Shipment.touch() 검색
→ 없음
→ 부모 BaseEntity.touch() 검색
→ 발견
→ 그 메서드 사용
→ 상속 관계는 Resolution 단계에서 처리.
for (int i = 0; i < 1000000; i++) {
shipment.calculate(100); // 100만 번 호출
}
매번 Resolution하면 너무 비싸. JVM은 한 번 Resolve된 결과를 인라인 캐시에 저장.
첫 호출: Resolution 수행 (느림)
이후 호출: 캐시된 메서드 직접 호출 (매우 빠름)
→ Resolution 비용은 첫 호출에서만. 이후는 빠른 호출.
1단계에서 결정된 메서드의 바이트코드를 실제로 실행하는 단계.
2단계 — Method Invocation 흐름:
┌─────────────────────────────────────────┐
│ 1. 메서드의 바이트코드 위치 확정 │
│ → Static Zone (static) 또는 │
│ Non-Static Zone (instance) │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 2. 새 스택 프레임 생성 │
│ - LVA 크기 결정 (메타데이터에서 읽음) │
│ - Operand Stack 초기화 │
│ - Return Address 저장 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 3. 매개변수 전달 │
│ - 호출자 Operand Stack → 새 LVA │
│ - 인스턴스 메서드면 this도 함께 │
└──────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 4. PC 이동 + 바이트코드 실행 시작 │
└─────────────────────────────────────────┘
→ Unit 1.4에서 본 스택 프레임 동작 + Unit 1.5의 invoke 명령이 결합.
public BigDecimal calculate(int weight) {
BigDecimal base = new BigDecimal("100");
return base.multiply(BigDecimal.valueOf(weight));
}
2단계 실행 흐름:
PC=0: new BigDecimal ← Heap에 BigDecimal 객체 생성
PC=N: ldc "100" ← 상수 풀에서 String 푸시
PC=N: invokespecial <init> ← 생성자 호출 (재귀적으로 2단계 발생)
PC=N: astore_2 ← base에 저장
PC=N: aload_2 ← base 로드
PC=N: iload_1 ← weight 로드
PC=N: invokestatic BigDecimal.valueOf ← 또 다른 메서드 호출 (재귀적 2단계)
PC=N: invokevirtual multiply ← 또 다른 메서드 호출
PC=N: areturn ← 결과 반환
→ 메서드 안에서 또 메서드를 호출하면 또 1+2단계 발생. 재귀적.
1. 반환값을 호출자의 Operand Stack에 push
2. 현재 프레임 pop
3. Return Address로 PC 복원
4. 호출자가 자기 코드 계속
→ Unit 1.4의 메서드 반환과 정확히 동일.
| Static Dispatch | Dynamic Dispatch | |
|---|---|---|
| 결정 시점 | 컴파일 타임 | 런타임 |
| 사용 명령 | invokestatic, invokespecial | invokevirtual, invokeinterface |
| 대상 | static, private, final, super, 생성자 | 일반 인스턴스 메서드 |
| 다형성 | ❌ | ✓ |
| 속도 | 빠름 | 빠름 (인라인 캐시 후) |
// 컴파일러가 알 수 있는 것:
Math.max(10, 20); // ← Math 클래스의 static max
super.toString(); // ← 부모 클래스 명확
new Shipment(); // ← 생성자, Shipment 클래스 명확
this.privateMethod(); // ← private, 다른 클래스에서 못 부름
final void finalMethod() {} // ← override 불가
이런 호출은 컴파일러가 정확한 메서드를 미리 알 수 있다.
→ 1단계 Resolution이 컴파일 타임에 끝남.
Cargo c = getCargo(); // 어떤 서브타입?
c.calculateFee(); // ← 컴파일러는 Cargo의 메서드인 줄만 앎
컴파일 시점:
바이트코드 → invokevirtual Cargo.calculateFee
런타임 시점:
c가 가리키는 객체의 Class Pointer 확인
→ DryCargo Klass의 VMT에서 calculateFee 슬롯 찾기
→ DryCargo.calculateFee 실제 실행
→ Unit 1.5의 VMT가 정확히 이 메커니즘.
public BigDecimal processCargo(Cargo cargo) {
BigDecimal fee = cargo.calculateFee(); // Dynamic (invokevirtual)
BigDecimal vat = Math.multiplyExact( // Static (invokestatic)
fee.longValue(), 10);
log.info("Fee: {}", fee); // Dynamic (invokeinterface)
return new BigDecimal(vat); // Static (invokespecial)
}
같은 메서드 안에서 4가지 invoke 모두 등장.
→ JVM이 매번 어떤 디스패치 방식을 쓸지 결정.
JIT 컴파일러는 일부 dynamic dispatch를 static처럼 최적화.
// 코드는 dynamic이지만
animal.sound();
// 만약 animal이 항상 Dog만 들어온다면
// JIT가 추론: "거의 항상 Dog이니, Dog.sound를 직접 부르자"
// → 인라이닝까지 가능
이게 인라인 캐시 + 인라이닝. 다형성의 성능 비용을 거의 0으로.
→ "다형성은 성능이 느리다"는 잘못된 통념. JIT 덕에 거의 차이 없음.
Method Area:
┌──────────────────────────────────────────────────┐
│ Shipment Klass │
│ ┌────────────────────────────────────────────┐ │
│ │ Class Metadata Zone │ │
│ │ • Method Table (시그니처 목록) │ │
│ │ [0] calculate(I)BigDecimal → ref #A │ │
│ │ [1] getId()Long → ref #B │ │
│ │ [2] setBlNo(String)V → ref #C │ │
│ │ [3] validate()V (private) → ref #D │ │
│ └────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ Non-Static Zone │ │
│ │ ref #A: calculate 바이트코드 │ │
│ │ 0: new │ │
│ │ 3: ldc │ │
│ │ ... │ │
│ │ ref #B: getId 바이트코드 │ │
│ │ 0: aload_0 │ │
│ │ ... │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
핵심 통찰:
JVM 내부에서 메서드 1개는 Method 구조체로 표현.
Method 구조체 (calculate 메서드):
├─ 이름: "calculate"
├─ 시그니처: "(I)Ljava/math/BigDecimal;"
├─ 접근 플래그: public, not static, not final
├─ 바이트코드 시작 주소: 0x...
├─ 바이트코드 크기: 47 bytes
├─ LVA 크기: 3 슬롯
├─ Operand Stack 최대 크기: 4
├─ Exception Table: (try-catch 정보)
└─ Local Variable Table: (디버깅 정보)
→ 1단계의 결과 = 이 Method 구조체.
→ 2단계는 이 구조체의 정보로 프레임 만들고 실행.
public interface Calculable {
BigDecimal calculate(int weight);
}
public class Shipment implements Calculable {
@Override
public BigDecimal calculate(int weight) { ... }
}
Shipment의 Method Table은:
Shipment Method Table:
[0] calculate(I)BigDecimal (Calculable 인터페이스 구현)
[1] getId()Long
...
invokeinterface로 호출 시:
Calculable c = new Shipment();
c.calculate(100);
→ c의 객체 → Shipment Klass
→ Shipment의 itable에서 Calculable 인터페이스 슬롯 찾기
→ 그 슬롯의 calculate 메서드 호출
추가 단계가 있어서 invokevirtual 보다 살짝 느리지만 실무에선 차이 무시 가능.
Unit 1.3 — Method Area의 3개 존
→ Metadata Zone에 시그니처, Static/Non-Static Zone에 바이트코드 분리
Unit 1.5 — Object Header의 Class Pointer
→ 객체 → Klass로 가는 다리
Unit 2.1 (이번) — Method Resolution & Invocation
→ 그 Klass의 Metadata에서 시그니처 찾고 → 바이트코드 실행
→ Phase 1의 메모리 구조 위에서 메서드 호출이 정확히 어떻게 일어나는지 추적 완성.
class Cargo {
public BigDecimal calculateFee() {
return BigDecimal.ZERO;
}
}
class DryCargo extends Cargo {
@Override
public BigDecimal calculateFee() {
return weight.multiply(new BigDecimal("100"));
}
}
JVM에서의 모습:
Cargo Klass:
Method Table:
[N] calculateFee → ref #C1 (Cargo의 바이트코드)
DryCargo Klass (Cargo 상속):
Method Table:
[N] calculateFee → ref #D1 (DryCargo의 바이트코드) ← 같은 슬롯 N
같은 시그니처, 같은 Method Table 슬롯 위치.
다른 클래스가 다른 바이트코드를 같은 슬롯에 매핑.
Cargo c = new DryCargo();
c.calculateFee();
1단계:
- 컴파일러: "Cargo.calculateFee를 호출하는 invokevirtual"
- 슬롯 N에서 메서드 호출하라는 명령
2단계:
- 런타임: c가 가리키는 객체의 Class Pointer 확인
- DryCargo Klass 발견
- DryCargo의 Method Table[N] = ref #D1
- ref #D1의 바이트코드 (DryCargo.calculateFee) 실행
핵심: 1단계는 "슬롯 N의 메서드"라고만 결정. 실제 어느 클래스의 메서드인지는 2단계에서 객체에 따라 결정.
가설: 컴파일 시점에 모든 게 결정된다면?
Cargo c = new DryCargo();
c.calculateFee(); // 컴파일러가 Cargo.calculateFee로 결정
// 이 경우 무조건 Cargo의 메서드 실행 → DryCargo 메서드 안 불림
// → 다형성 깨짐
→ 2단계 분리 = 다형성의 본질.
class DryCargo extends Cargo {
@Override
<public BigDecimal calculateFee() {
BigDecimal base = super.calculateFee(); // ← 부모 명시적 호출
return base.add(weight.multiply(...));
}
}
super.calculateFee()는 invokespecial 로 컴파일.
→ 동적 디스패치 안 함. 무조건 Cargo의 calculateFee 실행.
→ "이 객체가 사실 DryCargo이지만, Cargo의 메서드를 강제로 호출".
→ static dispatch의 정확한 사례. 1단계에서 결정 끝.
class Cargo {
public final BigDecimal calculateBase() {
return weight.multiply(BASE_RATE);
}
}
final 키워드는 "이 메서드는 오버라이드 불가" 라는 약속.
JIT는 이걸 보고 static dispatch처럼 처리 + 인라이닝.
→ 다형성 비용 0.
// JIT 최적화 결과 (개념적)
class Cargo {
public void doSomething(Cargo c) {
BigDecimal base = weight.multiply(BASE_RATE); // ← 인라인됨, 메서드 호출 사라짐
// ...
}
}
@Service
public class ShipmentService {
@Transactional
public Shipment save(Shipment s) {
return repository.save(s);
}
}
@Transactional이 어떻게 동작하나?
Spring의 답: 프록시 객체를 만들어 실제 ShipmentService 호출을 가로채기.
일반 호출:
controller → shipmentService.save()
→ ShipmentService 클래스의 save 메서드 직접 실행
AOP 호출:
controller → shipmentServiceProxy.save()
→ ShipmentService$$EnhancerBySpringCGLIB.save() 실행
1. 트랜잭션 시작
2. 진짜 ShipmentService.save() 호출
3. 트랜잭션 커밋/롤백
메모리에서 일어나는 일:
Heap:
ShipmentServiceProxy 객체 ← Controller가 받음
└─ target = 진짜 ShipmentService 객체
Method Area:
ShipmentService 클래스 (원본)
ShipmentService$$EnhancerBySpringCGLIB 클래스 (프록시)
└─ save() 시그니처가 같음 → invokevirtual로 호출되면 프록시 메서드 실행
→ Method Resolution은 그대로, Invocation 단계에서 프록시 메서드가 실행.
→ 2단계 분리가 만드는 메타 프로그래밍.
이 분리 메커니즘 때문에 흔히 마주치는 함정:
@Service
public class ShipmentService {
public void process() {
save(shipment); // ❌ AOP 동작 안 함!
}
@Transactional
public Shipment save(Shipment s) { ... }
}
왜?
shipmentService.save() → Spring 프록시 호출 → AOP 동작 ✓save() → this의 메서드 직접 호출 → 프록시 안 거침 → AOP 무시 ❌메모리 추적:
shipmentServiceProxy.process() 호출
→ 프록시 메서드 실행
→ 진짜 ShipmentService.process() 호출
→ 그 안의 save() 호출
→ this 가 진짜 객체이므로 this.save() 직접 호출
→ 프록시 안 거침
→ @Transactional 무시
해결:
// 1. 별도 빈으로 분리
@Service
public class TransactionalService {
@Transactional
public Shipment save(Shipment s) { ... }
}
@Service
public class ShipmentService {
private final TransactionalService tx;
public void process() {
tx.save(shipment); // ✓ 프록시 거침
}
}
// 2. 또는 self-injection (안티 패턴 가까움)
@Autowired
private ShipmentService self;
public void process() {
self.save(shipment); // ✓
}
→ 이 함정의 원인은 정확히 메서드 호출의 2단계 메커니즘 이해 부족.
@Entity
public class Shipment {
@OneToMany(fetch = FetchType.LAZY)
private List<Cargo> cargoes;
}
Shipment s = repository.findById(1L).get();
s.getCargoes().size(); // ← 이 시점에 DB 쿼리 발생
Hibernate의 트릭:
s.getCargoes()
→ Hibernate가 List 자리에 실제 ArrayList가 아닌 PersistentBag (Proxy) 넣어둠
→ PersistentBag이 List 인터페이스 구현
→ size() 호출 시 invokeinterface → PersistentBag.size()
→ "아직 로딩 안 됨, DB 쿼리 실행"
→ 결과 채우고 size 반환
invokeinterface의 2단계 분리가 Lazy Loading을 가능하게 함.
@Test
void test() {
ShipmentRepository mockRepo = Mockito.mock(ShipmentRepository.class);
when(mockRepo.findById(1L)).thenReturn(Optional.of(testShipment));
// mockRepo의 모든 메서드 호출이 가로채짐
mockRepo.findById(1L); // → 미리 설정한 값 반환
}
Mockito도 프록시 클래스를 동적 생성.
→ 1단계 Resolution은 정상, 2단계 Invocation에서 mock 동작.
@Configuration
public class AppConfig {
@Bean
public ShipmentService shipmentService() {
return new ShipmentService();
}
}
Spring은 AppConfig도 CGLIB 프록시로 감쌈.
→ shipmentService() 호출 시 단순히 새 객체 만들지 않고, 캐시된 싱글톤 반환.
@Bean
public Foo foo() {
return new Foo(bar()); // ← 매번 새 Bar?
}
@Bean
public Bar bar() {
return new Bar();
}
답: 아님. bar()도 프록시를 통해 호출 → 싱글톤 보장.
→ 2단계 분리로 가능한 메타 프로그래밍.
@Service
public class ShipmentService {
public void doAll() {
save(s); // ❌ @Transactional 무시
}
@Transactional
public void save(Shipment s) { ... }
}
→ 7.2 참조. 가장 흔한 함정.
@Service
public final class ShipmentService { // ❌ AOP 못 함
@Transactional
public final Shipment save(Shipment s) { ... }
}
Spring CGLIB AOP는 클래스 상속으로 프록시 생성.
final 클래스/메서드는 상속/오버라이드 불가 → 프록시 못 만듦.
해결: final 제거 또는 인터페이스 기반 JDK Proxy 사용.
@Service
public class ShipmentService {
@Transactional
private void save(Shipment s) { ... } // ❌ 무시됨
}
private 메서드는 invokespecial (static dispatch).
프록시가 가로챌 수 없음.
→ @Transactional 은 반드시 public 메서드에.
Method m = clazz.getMethod("calculate", int.class);
BigDecimal result = (BigDecimal) m.invoke(obj, 100);
Reflection 호출도 결국 invoke 명령으로 변환.
첫 호출은 Resolution이 비싸지만, 반복 호출은 캐시되어 빨라짐.
→ 한 번만 부르면 느림, 반복 호출은 일반 호출과 큰 차이 없음.
람다와 String concat이 사용하는 invokedynamic:
Runnable r = () -> System.out.println("hello");
내부적으로 invokedynamic → 1단계 Resolution을 런타임에 동적으로 수행.
→ 5번째 invoke (Java 7+, Unit 1.5에서 다룬 4종 외에 추가).
람다가 익명 클래스보다 메모리 효율이 좋은 이유.
// "다형성은 느리다" — 잘못된 인식
Animal a = getAnimal();
a.sound(); // 느릴 거라 생각하지만
JIT의 인라인 캐시 + devirtualization + 인라이닝.
→ 거의 항상 같은 타입이면 정적 호출만큼 빠름.
성능을 의심하기 전에 측정. 보통 다형성이 병목이 아님.
# 1. JIT가 어떤 호출을 인라이닝 했는지 보기
java -XX:+PrintInlining -XX:+UnlockDiagnosticVMOptions MyApp
# 2. 메서드 호출 카운트
java -XX:+CountInvocations MyApp
# 3. Async-Profiler로 호출 hot path 확인
async-profiler -d 30 -f profile.html <PID>
운영에서 메서드 호출 병목 의심 시 위 도구들로 측정.
| Q | 핵심 답변 |
|---|---|
| 메서드 호출의 2단계? | Resolution(시그니처 확인) + Invocation(바이트코드 실행) |
| 왜 2단계로 분리? | 다형성·AOP·동적 프록시 가능. lazy class loading |
| Static vs Dynamic Dispatch? | 컴파일 타임 결정 vs 런타임 결정 |
invokespecial 사용 시점? | 생성자, private, super 호출 |
| 다형성이 2단계에 의존하는 이유? | 1단계는 슬롯 결정, 2단계에서 객체 타입 따라 실제 메서드 |
| Spring AOP 동작 원리? | 프록시 객체로 invoke 단계에서 가로채기 |
| AOP self-invocation 함정? | this의 메서드 호출은 프록시 안 거침 |
| final 메서드 최적화? | static dispatch + 인라이닝 가능 |
| Reflection이 정말 느린가? | 첫 호출만 느림. 반복 호출은 일반 호출과 큰 차이 없음 |
| invokedynamic 사용처? | 람다, String concat, switch pattern (Java 21+) |
1. 메서드 호출은 반드시 2단계로 분리
2. Static vs Dynamic Dispatch
invokestatic, invokespecial)invokevirtual, invokeinterface)3. ILIC 실무 직결
이번 Unit에서 메서드 호출의 일반론을 봤다면, 다음은 Static과 Instance의 호출 경로를 정확히 비교.
✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
🚀 Phase 2 — JVM 메서드 실행 메커니즘
✅ Unit 2.1 메서드 호출의 2단계 처리 ← 여기
⏭ Unit 2.2 Static vs 인스턴스 호출 경로
⏭ Unit 2.3 인스턴스 메서드 호출의 전 과정 (Case Study)
⏭ Unit 2.4 new 연산자의 실제 동작
⏭ Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점
이번 Unit에서:
Phase 3에서:
#15가 정확히 무엇인지javap -v 로 클래스 파일 완전 분해→ Phase 2 끝나면 Phase 3 정점.