F-LAB JAVA · 2주차 · Phase 2 · JVM 메서드 실행 메커니즘
🏁 Phase 2 마지막 Unit
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
new Shipment() 한 줄이 하는 4가지 작업의 순서는?super() → 자식) 의 동작을 정확히 추적할 수 있는가?
new는 4가지 작업을 한 줄로 응축한 키워드다.
클래스 확인 → 메모리 할당 → 멤버 초기화 → 생성자 호출.
그러나 자바에는new외에도 객체를 만드는 5가지 우회 경로가 있고,
Spring·JPA·Jackson은 모두 이 우회 경로를 사용한다.
| new의 작업 | 자동차 비유 |
|---|---|
| ① 클래스 정보 확인 | 어떤 차종? 사양은? (스펙 확인) |
| ② Heap 메모리 할당 | 출고장에 차량 1대 공간 확보 |
| ③ 멤버 변수 초기화 | 차체 출고 (모든 부품 기본값) |
| ④ 생성자 호출 | 옵션 장착, 검수, 번호판 부착 |
중요: ②번 시점에 자동차는 이미 출고장에 있다. 그러나 ④번 끝나기 전까지는 운행 불가.
→ 생성자 호출 전 객체는 메모리에는 존재하지만 사용 불가 상태.
1. new의 본질 — 한 키워드, 4가지 작업
2. 작업 1 — 클래스 정보 확인
3. 작업 2 — Heap 메모리 할당
4. 작업 3 — 멤버 변수 초기화
5. 작업 4 — 생성자 호출
6. 생성자 체인 — super와 this
7. new 없이 객체 만들기 — 5가지 방법
8. ILIC 실무 — 객체 생성 비용과 최적화
9. 면접 질문 + 자기 점검 + Phase 2 정리
Shipment s = new Shipment("BL-001", LocalDate.now());
소스 코드: 1줄.
JVM 동작: 4단계 + 그 안의 다단계 작업.
┌─────────────────────────────────────────────┐
│ 작업 1: 클래스 정보 확인 │
│ - Method Area에서 Shipment Klass 확인 │
│ - 없으면 Class Loading 발동 │
├─────────────────────────────────────────────┤
│ 작업 2: Heap 메모리 할당 │
│ - 객체 크기 계산 │
│ - Eden Space (TLAB) 에서 공간 확보 │
│ - Object Header 설정 │
├─────────────────────────────────────────────┤
│ 작업 3: 멤버 변수 초기화 │
│ - 모든 인스턴스 변수를 기본값으로 │
│ - int=0, boolean=false, ref=null │
├─────────────────────────────────────────────┤
│ 작업 4: 생성자 호출 │
│ - super() 호출 (자동) │
│ - 인스턴스 초기화 블록 │
│ - 본문 코드 실행 │
└─────────────────────────────────────────────┘
시간 →
[작업 1] 클래스 확인 ──┐
▼
[작업 2] 메모리 할당 ──── 객체 등장 (단, 사용 불가)
│
[작업 3] 기본값 초기화 ──── 객체가 사용 가능 직전
│
[작업 4] 생성자 실행 ──── 객체 완전체
→ 작업 2 직후부터 객체는 메모리에 존재. 하지만 생성자 끝나기 전엔 완전한 상태가 아님.
Shipment s = new Shipment("BL-001", date);
0: new #2 // ← 작업 1, 2, 3 한 번에
3: dup
4: ldc #3 // "BL-001"
6: aload_X // date
8: invokespecial #4 // ← 작업 4 (생성자)
11: astore_1
new 바이트코드 명령어 하나가 작업 1~3을 통합 수행.
invokespecial이 작업 4 (생성자).
→ 객체 생성과 생성자 호출은 별개의 바이트코드.
→ 이 분리가 자바의 객체 생성 메커니즘의 핵심.
new Shipment(...) 실행 시점:
↓
JVM이 Method Area 검색
↓
Shipment Klass 발견?
Yes → 작업 2로
No → Class Loading 발동
Unit 2.3 Stage 1에서 본 3단계:
1. Loading
- .class 파일 찾기 (classpath 검색)
- 바이트 읽기
- Class 객체를 Heap에 생성
- Klass를 Method Area에 배치
2. Linking
- Verification: 바이트코드 검증 (악성 코드, 잘못된 점프 등)
- Preparation: static 필드 메모리 할당 + 기본값
- Resolution: 심볼 참조 해소 (lazy, 보통 처음 사용 시)
3. Initialization
- static 변수 초기값 대입
- static {} 블록 실행
public class Shipment extends BaseEntity {
// ...
}
Shipment 로딩 시:
1. BaseEntity 로딩되어 있나?
No → 먼저 BaseEntity 로딩 (재귀적)
Yes → 다음 단계
2. 인터페이스 로딩 (있으면)
3. Shipment 로딩
→ 부모 클래스 → 자식 클래스 순서.
→ Object 클래스는 JVM 시작 시 미리 로딩.
JLS(Java Language Specification)에 따른 클래스 Initialization 시점:
new T() — 인스턴스 생성T.staticMethod() — static 메서드 호출T.staticField 접근 (단, 상수 static final은 제외)T.class.newInstance() 등 Reflection→ 단순히 클래스 import만 해선 Initialization 안 됨.
→ 진짜 "사용" 시점에 실행.
public class Shipment {
static {
System.out.println("Shipment 클래스 초기화");
registerToFramework(); // 어떤 작업
}
private String blNo;
}
// 어디선가
new Shipment(); // ← 이 시점에 static {} 가 1번 실행
→ static 블록은 클래스당 1번만.
→ 비싼 작업을 여기 넣으면 첫 객체 생성 시 매우 느려질 수 있음.
이 시점에 보장되는 것:
✓ Shipment Klass가 Method Area에 있음
✓ Class Metadata Zone에 필드/메서드 정보
✓ Static Zone에 static 필드 + 메서드 바이트코드
✓ Non-Static Zone에 인스턴스 메서드 바이트코드
✓ static {} 실행 완료
이제 실제 객체를 만들 준비 완료.
public class Shipment {
private Long id; // 4 or 8 bytes (참조)
private String blNo; // 4 or 8 bytes
private int weight; // 4 bytes
private boolean delivered; // 1 byte
}
Compressed Oops 활성화 가정:
Header: 16 bytes (Mark Word 8 + Class Pointer 4 + Padding 4)
id (참조): 4 bytes
blNo (참조): 4 bytes
weight: 4 bytes
delivered: 1 byte
Padding: 3 bytes (8-byte 정렬)
─────────────────────────────
총: 32 bytes
→ JVM이 클래스 정보로 자동 계산. 객체마다 일정 크기.
Heap의 Young Generation:
┌──────────────────────────────────────────┐
│ Eden Space │
│ ┌──────┬──────┬──────┬──────┬──────┐ │
│ │ obj1 │ obj2 │ obj3 │ │ │ │
│ └──────┴──────┴──────┴──────┴──────┘ │
│ ↑ │
│ 다음 할당 위치 │
└──────────────────────────────────────────┘
JVM은 Bump Allocation 패턴:
1. 현재 할당 포인터 위치 확인
2. 객체 크기만큼 포인터를 이동시킬 수 있나? (= 공간 충분?)
Yes → 포인터 이동 + 객체 메모리 확정
No → Minor GC 발동 → 재시도
→ 객체 할당은 평균 매우 빠름 (포인터 +N).
→ GC가 발동하면 비용 폭증.
Eden Space:
┌──────────────────────────────────────────┐
│ Thread A의 TLAB │ Thread B의 TLAB │ ... │
└──────────────────────────────────────────┘
각 스레드는 자기 TLAB(Thread Local Allocation Buffer) 안에서:
→ Unit 1.5에서 본 메커니즘. 동시 객체 생성이 빠른 비밀.
할당된 메모리는 0으로 초기화.
할당 직후 메모리:
Header 영역: (잠시 후 설정)
Instance 영역: 모두 0x00으로 채워짐
→ 작업 3 (멤버 초기화)이 이미 부분적으로 처리된 셈.
Object Header (16 bytes):
┌─────────────────────────┐
│ Mark Word (8 bytes) │
│ 초기 상태: │
│ - hashcode 미계산 │
│ - GC age = 0 │
│ - unlocked │
├─────────────────────────┤
│ Class Pointer (4 bytes) │ ← Shipment Klass 주소
└─────────────────────────┘
→ 이 시점에 객체 = 클래스 연결 완료.
→ getClass(), instanceof 등이 작동 가능.
Heap:
┌──────────────────────────┐
│ Shipment @0x7f4a2c01 │
│ Mark Word: 초기 상태 │
│ Class ptr → Shipment │
│ id = null │ ← 0이지만 참조 타입이라 null
│ blNo = null │
│ weight = 0 │
│ delivered = false │
└──────────────────────────┘
중요: 이 시점에 객체는 이미 메모리에 존재.
getClass() 가능 → ShipmentClass 반환hashCode() 가능 → 새 해시 계산엄밀히 말하면 작업 2(메모리 할당 + 0 클리어)가 작업 3의 절반.
정확한 의미:
= ... 값public class Shipment {
private int weight = 100; // ← 명시적 초기값
private LocalDate createdAt = LocalDate.now(); // ← 명시적 초기값
private List<Cargo> cargoes = new ArrayList<>(); // ← 명시적 초기값
private String blNo; // (기본값 null)
}
컴파일러가 이 초기값들을 생성자의 시작 부분에 자동 삽입.
// 컴파일러가 만드는 모습 (개념적)
public Shipment() {
super(); // 1) 부모 생성자
this.weight = 100; // 2) 필드 초기값
this.createdAt = LocalDate.now();
this.cargoes = new ArrayList<>();
// this.blNo = null (이미 0 클리어로 처리됨)
// 3) 사용자 생성자 본문 (있다면)
}
public class Shipment {
private final String id;
{ // ← 인스턴스 초기화 블록 (생성자 아님)
System.out.println("Shipment 생성 중");
if (someCondition) {
// 어떤 작업
}
}
public Shipment() {
// ...
}
}
블록 {} 가 모든 생성자의 시작 부분에 자동 삽입.
// 컴파일러가 만드는 모습
public Shipment() {
super();
// 필드 초기값
// 인스턴스 초기화 블록
{
System.out.println("Shipment 생성 중");
// ...
}
// 생성자 본문
}
1. 부모 클래스의 필드 초기값 + 초기화 블록 + 생성자 (재귀)
2. 자식 클래스의:
a. 필드 선언의 명시적 초기값
b. 인스턴스 초기화 블록 {}
c. 생성자 본문
순서가 중요. 부모가 항상 먼저.
public class Shipment {
private final String blNo; // ← 반드시 초기화 필요
private final LocalDate eta;
public Shipment(String blNo, LocalDate eta) {
this.blNo = blNo; // ✓ 생성자에서
this.eta = eta;
}
}
final 필드 규칙:
→ 불변 객체(Immutable) 패턴의 기초.
new Shipment("BL-001", date);
바이트코드:
0: new #2 // 작업 1, 2, 3
3: dup
4: ldc #3 // "BL-001"
6: aload_X // date
8: invokespecial #4 // 작업 4: 생성자 호출 ★
→ invokespecial 이 작업 4의 핵심.
→ Unit 1.5의 invokespecial이 정확히 여기서 사용됨 (생성자, private, super).
1. Operand Stack에서 인자들 pop
Stack: [obj_ref, "BL-001", date_ref] → 모두 가져감
2. 새 스택 프레임 생성:
<init>() 프레임
LVA[0] this = obj_ref ← 작업 2에서 만든 객체
LVA[1] blNo = "BL-001"
LVA[2] eta = date_ref
3. 생성자 바이트코드 실행
public Shipment(String blNo, LocalDate eta) {
this.blNo = blNo;
this.eta = eta;
validate();
}
바이트코드 (대략):
0: aload_0 // this
1: invokespecial #1 // super() — Object의 <init>
4: aload_0 // this
5: aload_1 // blNo
6: putfield #2 // this.blNo = blNo
9: aload_0
10: aload_2 // eta
11: putfield #3 // this.eta = eta
14: aload_0
15: invokespecial #4 // this.validate()
18: return
핵심 관찰:
super() 호출 (자동 삽입)public class Shipment extends BaseEntity {
public Shipment() {
// super() — 컴파일러가 자동 추가
// 그 다음 본문 코드
}
}
명시 안 해도 컴파일러가 자동으로 추가.
예외 1: 명시적으로 부모 생성자 호출
public Shipment(String blNo) {
super(blNo); // 명시
// ...
}
예외 2: this() 호출 시
public Shipment() {
this("DEFAULT"); // 다른 생성자 호출 (super 자동 안 됨)
}
public Shipment(String blNo) {
super();
this.blNo = blNo;
}
this() 와 super() 는 둘 다 생성자의 첫 줄이어야 함.
Heap:
┌──────────────────────────┐
│ Shipment @0x7f4a2c01 │
│ Mark Word │
│ Class ptr → Shipment │
│ blNo = "BL-001" │ ← 생성자가 설정
│ eta = 2024-12-01 │ ← 생성자가 설정
│ weight = 100 │ ← 필드 초기값
│ cargoes = [] (ArrayList) │
│ delivered = false │
└──────────────────────────┘
이제 외부에서 사용 가능 (메서드 호출, 필드 접근 등).
new 표현식의 결과로 이 객체의 참조가 Operand Stack에 남음 (dup 덕분).
class Animal {
String species;
Animal() {
this.species = "unknown";
System.out.println("Animal()");
}
Animal(String species) {
this.species = species;
System.out.println("Animal(" + species + ")");
}
}
class Dog extends Animal {
String name;
Dog() {
super(); // 자동
System.out.println("Dog()");
}
Dog(String name) {
super("Dog"); // 명시
this.name = name;
System.out.println("Dog(" + name + ")");
}
Dog(String name, String species) {
this(name); // 다른 생성자 호출
// species는 무시 (예시)
}
}
new Dog("Rex");
호출 흐름:
Dog("Rex") 시작
↓ super("Dog")
Animal("Dog") 시작
↓ super()
Object() 시작
Object() 완료
Animal("Dog") 본문 실행
this.species = "Dog"
print "Animal(Dog)"
Animal("Dog") 완료
↓
Dog("Rex") 본문 실행
this.name = "Rex"
print "Dog(Rex)"
Dog("Rex") 완료
출력:
Animal(Dog)
Dog(Rex)
→ 항상 부모가 먼저, 자식이 나중.
new Dog("Rex");
호출 깊이가 깊은 어느 순간 (Animal의 본문 실행 중):
Stack:
┌────────────────────────────┐
│ Animal.<init>(String) 프레임 │ ← 현재 실행 중
│ LVA[0] this = obj_ref │
│ LVA[1] species = "Dog" │
├────────────────────────────┤
│ Dog.<init>(String) 프레임 │
│ LVA[0] this = obj_ref │ ← 같은 this!
│ LVA[1] name = "Rex" │
├────────────────────────────┤
│ 호출자 프레임 (main 등) │
└────────────────────────────┘
Heap:
Dog 객체 @0x7f4a2c01 (Animal의 species 필드 설정 중)
핵심: 부모 생성자와 자식 생성자가 같은 this 참조를 공유.
→ 같은 객체를 단계적으로 초기화.
class Shipment {
private String blNo;
private LocalDate eta;
private BigDecimal freight;
public Shipment(String blNo) {
this(blNo, LocalDate.now()); // 두 인자 생성자 호출
}
public Shipment(String blNo, LocalDate eta) {
this(blNo, eta, BigDecimal.ZERO); // 세 인자 생성자 호출
}
public Shipment(String blNo, LocalDate eta, BigDecimal freight) {
super();
this.blNo = blNo;
this.eta = eta;
this.freight = freight;
}
}
호출:
new Shipment("BL-001");
흐름:
Shipment("BL-001") 시작
↓ this("BL-001", LocalDate.now())
Shipment("BL-001", now) 시작
↓ this("BL-001", now, ZERO)
Shipment("BL-001", now, ZERO) 시작
↓ super()
Object() 실행 완료
this.blNo = "BL-001"
this.eta = now
this.freight = ZERO
Shipment 3개 인자 완료
Shipment 2개 인자 완료
Shipment 1개 인자 완료
→ 모든 작업이 최종적으로 한 객체에 누적.
class Parent {
public Parent() {
init(); // ❌ 가상 메서드 호출
}
protected void init() {
System.out.println("Parent init");
}
}
class Child extends Parent {
private String name;
public Child() {
super(); // Parent() 호출 → init() 호출 → Child의 init() 실행!
this.name = "Rex";
}
@Override
protected void init() {
System.out.println("Child init: " + name); // ❌ name 아직 null!
}
}
문제:
→ 생성자에서 오버라이드 가능한 메서드 호출 금지 (Effective Java Item 19).
해결:
Class<?> clazz = Class.forName("com.ilic.Shipment");
Shipment s = (Shipment) clazz.getDeclaredConstructor().newInstance();
// 또는 인자가 있다면
Shipment s2 = (Shipment) clazz
.getDeclaredConstructor(String.class)
.newInstance("BL-001");
동작:
→ new와 동일한 4단계 모두 수행. 단지 컴파일 타임이 아닌 런타임에 결정.
사용처:
public class Shipment implements Cloneable {
@Override
public Shipment clone() throws CloneNotSupportedException {
return (Shipment) super.clone();
}
}
Shipment original = new Shipment("BL-001");
Shipment copy = original.clone(); // ← new 없이 객체 생성!
동작:
public class Shipment implements Cloneable {
private String blNo;
public Shipment() {
this.blNo = "DEFAULT"; // 생성자에서 설정
System.out.println("Constructor called");
}
}
Shipment a = new Shipment(); // 출력: "Constructor called"
Shipment b = a.clone(); // 아무 출력 없음
b.getBlNo(); // "DEFAULT" (필드 복사됨)
→ 생성자 우회. 디자인 시 주의.
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
private String blNo;
}
// 직렬화 후 역직렬화
byte[] bytes = serialize(shipment);
Shipment restored = (Shipment) deserialize(bytes);
// ↑ new 없이 객체 생성
동작:
→ 1주차 Unit 7.4의 Serializable 함정 중 하나.
→ 생성자에서 했던 검증이 우회됨 → 보안 사고 가능.
import sun.misc.Unsafe;
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Shipment s = (Shipment) unsafe.allocateInstance(Shipment.class);
동작:
→ 가장 원시적. 위험.
→ 객체가 미초기화 상태 (모든 참조 필드는 null, 숫자는 0).
→ 일반 코드에선 절대 사용 금지.
사용처 (라이브러리 내부):
// JDK Dynamic Proxy
Shipment proxy = (Shipment) Proxy.newProxyInstance(
classLoader,
new Class[] { Shipment.class },
(proxyObj, method, args) -> {
// 가로채기
return realObj.invoke(method, args);
}
);
// CGLIB
Shipment proxy = (Shipment) Enhancer.create(
Shipment.class,
(MethodInterceptor) (obj, method, args, methodProxy) -> {
// 가로채기
}
);
동작:
→ Spring AOP, Mockito, Hibernate Proxy의 기반.
→ 박승제씨가 이미 코드에서 매일 마주치는 메커니즘.
| 방법 | 생성자 호출? | 사용처 |
|---|---|---|
new | ✓ | 일반 코드 |
| Reflection | ✓ | Spring DI, JUnit, Jackson |
clone() | ❌ | 복사 패턴 |
| Deserialization | ❌ | Serializable, Caching |
Unsafe.allocateInstance | ❌ | 라이브러리 내부 |
| Dynamic Proxy / CGLIB | ✓ (다른 클래스의) | AOP, Mockito |
→ 현대 자바 앱은 new 외의 경로가 더 많음.
생성자가 호출되기 전에 객체는 이미 메모리에 존재하는가?
답: 예. 작업 2 직후에 메모리에는 존재. 단, 작업 4(생성자) 전엔 사용 가능 상태는 아님.
→ Unsafe.allocateInstance는 정확히 작업 4를 건너뛴 상태의 객체를 만든다.
@Benchmark
public Shipment createShipment() {
return new Shipment("BL-001", LocalDate.now());
}
JMH 결과 (가상):
Cost: ≈ 30 ns per allocation
- Bump allocation: ~10ns
- Header 설정: ~5ns
- 생성자 실행: ~15ns
→ 일반적인 객체 생성은 30ns 정도.
→ 초당 3천만 개 생성 가능.
대부분 아니다.
→ 객체 생성을 줄여서 얻는 이득은 거의 항상 다른 곳에서 더 큰 손실.
그러나 다음 경우는 다름:
public class ConnectionPool {
private final BlockingQueue<Connection> pool;
public Connection borrow() {
Connection conn = pool.poll();
if (conn == null) {
conn = createNew(); // 새로 만들기
}
return conn;
}
public void release(Connection conn) {
pool.offer(conn); // 풀에 반환
}
}
언제 효과적?
언제 비효율?
→ 박승제씨가 직접 풀링할 일은 거의 없음. JDBC, ThreadPool은 이미 풀링.
public class Status {
public static final Status ACTIVE = new Status("ACTIVE");
public static final Status INACTIVE = new Status("INACTIVE");
public static final Status DELETED = new Status("DELETED");
private final String code;
private Status(String code) { this.code = code; }
}
→ 객체를 미리 만들어 공유.
→ Integer.valueOf의 캐시(-128 ~ 127)와 같은 원리 (Unit 1.6).
ILIC 적용:
public enum ShipmentStatus {
DRAFT, IN_TRANSIT, DELIVERED, CANCELLED
}
enum은 자동 Flyweight. 4개 객체만 메모리에 존재.
// ❌ 매번 새 StringBuilder
public String generateBlNo(List<String> parts) {
StringBuilder sb = new StringBuilder();
for (String p : parts) sb.append(p);
return sb.toString();
}
// ✓ ThreadLocal로 재사용
private static final ThreadLocal<StringBuilder> SB_CACHE =
ThreadLocal.withInitial(StringBuilder::new);
public String generateBlNo(List<String> parts) {
StringBuilder sb = SB_CACHE.get();
sb.setLength(0); // 재사용
for (String p : parts) sb.append(p);
return sb.toString();
}
→ 미세 최적화. 정말 hot path에서만.
→ ThreadLocal 누수 위험 주의.
public BigDecimal calculate(int weight, int rate) {
BigDecimal w = new BigDecimal(weight); // 메서드 내부에서만 사용
BigDecimal r = new BigDecimal(rate);
return w.multiply(r); // 반환은 결과만
}
JIT의 분석:
w, r은 메서드 밖으로 escape 안 함→ JIT가 자동으로 객체 → 스택 할당 변환.
→ 박승제씨는 코드를 자연스럽게 쓰면 JIT가 알아서 최적화.
일반적인 Spring Boot 앱의 객체 생성:
- 요청당 ~수십 ~ 수백 개 객체
- 대부분 짧은 수명 (Young Gen에서 회수)
- GC가 효율적으로 처리
문제가 생기는 경우:
- 캐시가 너무 자랑 (Old Gen 압박)
- 큰 객체 생성 (배열, 컬렉션)
- GC 로그에서 Full GC 빈번
우선 측정, 그 다음 최적화. 80%는 측정 후 다른 곳이 병목.
| Q | 핵심 답변 |
|---|---|
new의 4가지 작업? | 클래스 확인 → 메모리 할당 → 필드 초기화 → 생성자 호출 |
| 생성자 호출 전 객체 존재? | 예. 작업 2 직후 메모리에 있음. 단 사용 가능 상태는 아님 |
new 바이트코드와 invokespecial 관계? | new는 작업 1~3, invokespecial이 작업 4 (생성자) |
dup이 필요한 이유? | new 객체 참조를 invokespecial이 소비. astore도 써야 해서 복제 |
| super() 자동 호출 시점? | 생성자 첫 줄. this() 호출 시 제외 |
| 생성자 체인 순서? | 부모가 먼저 (Object → ... → 자식) |
| new 없이 객체 만드는 5가지? | Reflection, clone, Deserialization, Unsafe, Dynamic Proxy |
| 생성자에서 오버라이드 메서드 호출 위험? | 자식의 메서드가 호출됨. 자식 필드 초기화 전 |
| 객체 생성 비용? | 일반 ~30ns. 거의 항상 다른 곳이 병목 |
| Escape Analysis? | JIT가 메서드 안에서만 쓰는 객체를 Stack에 할당 |
Phase 2에서 본 것 (Unit 2.1 ~ 2.4)
Unit 2.1 — 메서드 호출의 2단계 처리
↓ Resolution (시그니처 확인) + Invocation (바이트코드 실행)
Unit 2.2 — Static vs 인스턴스 호출 경로
↓ 4가지 케이스, this의 전달 메커니즘
Unit 2.3 — 인스턴스 메서드 호출의 전 과정 (Case Study)
↓ 5단계 추적, JIT의 1000배 최적화
Unit 2.4 — new 연산자의 실제 동작
↓ 4가지 작업, new 없는 5가지 객체 생성
즉답할 수 있어야 할 질문:
Model2 m2 = new Model2(); m2.hap(1, 2); 두 줄이 JVM에서 어떤 단계를 거치는가?모두 답할 수 있다면 Phase 2 졸업. Phase 3 (정점) 진입.
1. new는 4가지 작업의 응축
new (작업 1~3) + invokespecial (작업 4)2. 생성자 체인은 부모부터
3. new 외에 5가지 우회 경로
이번 Unit까지 Phase 2를 완주했다면, Phase 3가 모든 것의 정점이다.
지금까지 본 모든 메커니즘:
invoke 4종, dup, new, invokespecial, ...#2, #3, #15 같은 상수 풀 인덱스이게 모두 javap -v Shipment.class 한 줄로 직접 확인 가능.
Phase 3에서 박승제씨는 눈으로 바이트코드를 읽는 능력을 갖게 된다.
✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
🎯 Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점
⏭ Unit 3.1 바이트코드란 무엇인가
⏭ Unit 3.2 상수 풀의 생성과 구조
⏭ Unit 3.3 심볼 참조
⏭ Unit 3.4 바이트코드 실전 분석
⏭ Phase 4 — G1 GC 심화
⏭ Phase 5 — 컬렉션 내부 구조
⏭ Phase 6 — Reflection & Iterator
⏭ Phase 7 — Buffer
Phase 1 (메모리 매핑):
✅ Unit 1.1 자바 변수의 3종류
✅ Unit 1.2 변수별 저장 위치
✅ Unit 1.3 Method Area의 3개 존 ★
✅ Unit 1.4 Stack Area의 동작
✅ Unit 1.5 Heap과 객체-Metadata 연결
✅ Unit 1.6 Literal Pool Area
Phase 2 (메서드 실행):
✅ Unit 2.1 메서드 호출의 2단계 처리
✅ Unit 2.2 Static vs 인스턴스 호출 경로
✅ Unit 2.3 인스턴스 메서드 호출의 전 과정
✅ Unit 2.4 new 연산자의 실제 동작 ← 여기, Phase 2 완주
10개 Unit. 2주차의 절반.