2주차 Unit 1.4 — Stack Area의 동작

Psj·2026년 5월 15일

F-lab

목록 보기
53/197

Unit 1.4 — Stack Area의 동작

F-LAB JAVA · 2주차 · Phase 1 · 자바 변수 ↔ 메모리 영역의 매핑


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 스택 프레임의 3개 구성 요소(Local Variable Array · Operand Stack · Frame Data)는?
  • int sum = a + b; 한 줄이 Operand Stack에서 어떻게 처리되는가?
  • PC Register는 무엇을 가리키며, 왜 스레드별로 있어야 하는가?
  • 메서드가 끝나면 반환값이 어떻게 호출자에게 전달되는가?
  • StackOverflowError가 발생하는 정확한 메커니즘은?
  • Stack Trace의 각 줄은 어디서 오는 정보인가?
  • 운영 서버 thread dump를 어떻게 읽어야 하는가?

🎯 핵심 한 문장

Stack은 "메서드 호출의 발자취"를 담는 LIFO 자료구조다.
호출 한 번 = 프레임 push, 반환 한 번 = 프레임 pop.
각 프레임 안에는 지역 변수 배열 · 피연산자 스택 · 프레임 데이터가 있고,
JVM의 모든 산술/논리 연산은 피연산자 스택 위에서 일어난다.

비유 — 책상 위의 메모지 더미

Stack 요소비유
Stack책상 위에 쌓은 메모지 더미 (LIFO)
Stack Frame메모지 1장 (메서드 1번 호출 = 1장)
Local Variable Array메모지의 상단 — 변수 이름과 값 (이 회의의 참석자/안건)
Operand Stack메모지의 하단 — 계산용 임시 메모 (계산 중인 숫자)
Frame Data메모지의 모서리 — 끝나면 어디로 돌아갈지 표시
PC Register펜이 가리키는 줄 — "지금 여기 읽는 중"

회의(메서드)가 끝나면 그 메모지(프레임)를 통째로 버리고, 이전 회의 메모지를 다시 봄.


🧭 9개 섹션 로드맵

1. Stack 복습 + 정밀화        — 1.2/1.3에서 본 것 위에 쌓기
2. 스택 프레임의 3가지 구성 요소
3. Local Variable Array      — 지역 변수의 진짜 저장 방식
4. Operand Stack             — JVM은 스택 머신이다
5. PC Register와 명령 추적    — 한 줄씩 명령을 따라가는 메커니즘
6. 메서드 호출과 반환의 전체 흐름
7. 멀티스레드와 Stack         — 컨텍스트 스위칭의 핵심
8. ILIC 실무 + 디버깅          — Stack Trace · Thread Dump 읽기
9. 면접 질문 + 자기 점검

1️⃣ Stack 복습 + 정밀화

1.1 1.2 / 1.3에서 본 것

Stack:
  ┌───────────────────────┐
  │ add() 프레임           │  ← 현재 실행 중
  │   매개변수, 지역변수    │
  ├───────────────────────┤
  │ run() 프레임           │
  ├───────────────────────┤
  │ main() 프레임          │
  └───────────────────────┘

→ 메서드 호출마다 프레임이 쌓이고, 종료하면 사라진다.
→ 지역 변수와 매개변수가 그 안에 있다.

이번 Unit에서 정밀화할 것:

  • 프레임 안에 정확히 무엇이 들어있는가
  • 어떻게 계산이 일어나는가
  • 다음 명령은 어떻게 찾는가

1.2 한 단계 더 들어간 그림

Stack Frame 1개:
┌────────────────────────────────────┐
│ 1. Local Variable Array            │  ← 매개변수 + 지역 변수 (배열 형태)
│    [0]: this                       │
│    [1]: weight (매개변수)            │
│    [2]: base (지역변수)              │
│    [3]: ...                        │
├────────────────────────────────────┤
│ 2. Operand Stack                   │  ← 계산용 임시 스택
│    (현재 비어있음 또는 일부 값)        │
├────────────────────────────────────┤
│ 3. Frame Data                      │  ← 메타 정보
│    Return Address: 호출자의 PC      │
│    상수 풀 참조 등                   │
└────────────────────────────────────┘

→ 이 3개가 모든 스택 프레임의 표준 구조.


2️⃣ 스택 프레임의 3가지 구성 요소

2.1 Local Variable Array (LVA)

지역 변수와 매개변수의 저장소. 배열 형태로 관리.

public BigDecimal calculate(int weight, BigDecimal rate) {
    BigDecimal base = new BigDecimal("100");
    BigDecimal result = base.multiply(rate);
    return result;
}

LVA의 모습:

Local Variable Array (인덱스 기반):
  [0] this        ← 인스턴스 메서드의 숨겨진 첫 매개변수
  [1] weight      ← 매개변수 1
  [2] rate        ← 매개변수 2
  [3] base        ← 지역 변수
  [4] result      ← 지역 변수

static 메서드면 [0]부터 첫 매개변수가 시작.

바이트코드에서의 모습 (Phase 3 미리보기):

iload_1     ← LVA[1] 로드 (weight)
aload_2     ← LVA[2] 로드 (rate)
astore_3    ← LVA[3]에 저장 (base)

iload_X, astore_X 같은 명령이 LVA의 인덱스로 변수에 접근한다.

2.2 Operand Stack (피연산자 스택)

JVM이 산술/논리 연산을 수행하는 임시 작업 공간.

int sum = 1 + 2;

Operand Stack 동작:

Step 1: iconst_1     [1]            ← 1을 푸시
Step 2: iconst_2     [1, 2]         ← 2를 푸시
Step 3: iadd         [3]            ← 위 2개를 꺼내 더한 값 푸시
Step 4: istore_1     []             ← 결과를 LVA[1]에 저장

핵심: JVM은 레지스터 머신이 아니라 스택 머신.

  • x86 CPU: add eax, ebx (레지스터끼리 직접 연산)
  • JVM: 값을 스택에 푸시 → 연산 명령 → 결과가 스택에 남음

2.3 Frame Data

프레임의 메타 정보가 들어가는 영역.

  • Return Address: 메서드 종료 시 호출자의 어느 명령으로 돌아갈지 (PC 값)
  • 상수 풀 참조: 이 메서드가 속한 클래스의 상수 풀 위치
  • 예외 테이블 참조: try-catch의 catch 블록 위치

→ 이 영역은 JVM 구현체마다 세부 구조가 다름. 개념적으로 이해하면 충분.

2.4 정리 — 프레임 1개의 풀 그림

public class App {
    public static int add(int a, int b) {
        int sum = a + b;
        return sum;
    }
}

add(10, 20) 호출 직후, 끝나기 직전의 프레임:

add() 프레임:
┌──────────────────────────────────┐
│ Local Variable Array              │
│   [0] a   = 10                   │  ← (static이라 this 없음)
│   [1] b   = 20                   │
│   [2] sum = 30                   │
├──────────────────────────────────┤
│ Operand Stack                     │
│   [bottom] ... [top: 30]         │  ← ireturn 직전, 반환값 푸시 상태
├──────────────────────────────────┤
│ Frame Data                        │
│   Return Address → 호출자 PC      │
│   상수 풀 참조                     │
└──────────────────────────────────┘

3️⃣ Local Variable Array — 지역 변수의 진짜 저장 방식

3.1 배열 인덱스로 변수 접근

JVM 입장에서 변수 이름은 컴파일 시점에 사라진다. 바이트코드에는 인덱스만 남는다.

public void process(int x, int y) {
    int sum = x + y;
    String msg = "result";
}

LVA:

[0] this      ← 인스턴스 메서드의 숨은 매개변수
[1] x         ← 첫 매개변수
[2] y         ← 두 번째 매개변수
[3] sum       ← 지역 변수 1
[4] msg       ← 지역 변수 2

→ "x" 라는 이름은 디버깅 정보에만 남음 (javac -g).
→ 실제 실행은 인덱스로.

3.2 long / double은 2칸 차지

public void method(int a, long b, double c, int d) {}

LVA:

[0] this
[1] a       ← int, 1칸
[2] b       ← long, 2칸 차지 시작
[3]   "     ← (b의 두 번째 칸)
[4] c       ← double, 2칸 차지 시작
[5]   "     ← (c의 두 번째 칸)
[6] d       ← int, 1칸

longdouble은 64비트라 2개의 슬롯 사용.
→ 면접에선 잘 안 나오지만, 바이트코드 읽을 때 알아두면 좋음.

3.3 슬롯 재사용

public void method() {
    {
        int a = 10;     // LVA[1] 사용
    }                   // 블록 끝 → a 사라짐
    {
        String b = "x"; // LVA[1] 재사용 가능!
    }
}

LVA의 슬롯은 스코프가 끝나면 재사용된다.
→ 메서드 안에서 변수 100개 선언해도, 같은 시점에 살아있는 것만 슬롯 차지.
→ 컴파일러가 최적화.

3.4 디버깅 시 — LocalVariableTable

javac -g App.java          # 디버그 정보 포함
javap -v -p App.class

출력 일부:

LocalVariableTable:
  Start  Length  Slot  Name   Signature
      0       6     0  this   LApp;
      0       6     1  x      I
      0       6     2  y      I
      3       3     3  sum    I

Slot이 LVA 인덱스, Signature가 타입(I=int, L...;=object).
→ IDE 디버거가 보여주는 변수 이름의 출처.


4️⃣ Operand Stack — JVM은 스택 머신이다

4.1 왜 스택 머신인가

JVM의 모든 계산은 피연산자 스택 위에서 일어난다.

레지스터 머신 (x86 CPU):

add eax, ebx    ; eax = eax + ebx

스택 머신 (JVM):

iload_1         ; 첫 번째 값을 스택에 푸시
iload_2         ; 두 번째 값을 스택에 푸시
iadd            ; 위 두 개를 꺼내 더하고 결과를 푸시
istore_3        ; 스택 top을 LVA[3]에 저장

장점:

  • CPU 독립적: 어떤 CPU(x86, ARM, RISC-V)에서도 같은 바이트코드 동작
  • 단순한 명령어 집합: 레지스터 할당 불필요
  • 컴파일러 단순화: 표현식 트리 → 스택 명령으로 직선 변환

단점:

  • 명령어 수가 많음 (레지스터 머신보다)
  • → JIT 컴파일러가 런타임에 레지스터 기반 네이티브 코드로 최적화

4.2 핵심 명령어 카테고리

카테고리명령어 예동작
상수 로드iconst_0, iconst_1, bipush 100스택에 상수 푸시
변수 로드iload_1, aload_2, lload_3LVA → 스택 푸시
변수 저장istore_1, astore_2스택 pop → LVA 저장
산술iadd, isub, imul, idiv스택 top 2개 연산
메서드 호출invokestatic, invokevirtual, invokespecial스택 인자 사용, 결과 푸시
반환ireturn, areturn, return스택 top을 호출자 스택으로
분기ifeq, ifne, gotoPC 점프

i = int, a = address(reference), l = long, f = float, d = double.

4.3 추적 예시 — int sum = a + b;

public int add(int a, int b) {
    int sum = a + b;
    return sum;
}

바이트코드:

0: iload_1        // a 로드
1: iload_2        // b 로드
2: iadd           // a + b
3: istore_3       // sum에 저장
4: iload_3        // sum 로드
5: ireturn        // 반환

add(10, 20) 호출 시 Operand Stack 변화:

LVA: [this=ref, a=10, b=20, sum=?]

PC=0: iload_1
  Stack: [10]                       ← a를 푸시

PC=1: iload_2
  Stack: [10, 20]                   ← b를 푸시

PC=2: iadd
  Stack: [30]                       ← 10과 20을 꺼내 더하고 30 푸시

PC=3: istore_3
  Stack: []                         ← 30 꺼내서 LVA[3]에 저장
  LVA:   [this, 10, 20, 30]

PC=4: iload_3
  Stack: [30]                       ← LVA[3] 푸시

PC=5: ireturn
  Stack: [] (호출자 스택에 30 푸시)   ← 30을 반환

모든 연산이 스택 푸시 / 팝 패턴.

4.4 메서드 호출도 스택 위에서

String s = "Hello".toUpperCase();

바이트코드:

0: ldc       "Hello"                // 상수 풀에서 "Hello" 로드
2: invokevirtual #N // toUpperCase   // top의 String 객체로 메서드 호출
5: astore_1                          // 결과 저장
PC=0: ldc "Hello"
  Stack: ["Hello"]

PC=2: invokevirtual toUpperCase
  - "Hello" pop
  - toUpperCase() 호출 (새 프레임 push)
  - 반환값 "HELLO" 를 우리 스택에 push
  Stack: ["HELLO"]

PC=5: astore_1
  Stack: []
  LVA: [this, "HELLO"]

메서드 호출 = 인자를 스택에 쌓고 invoke 명령 → 호출된 메서드는 그 인자들을 LVA로 받음 → 반환값은 호출자 스택에 다시 push.


5️⃣ PC Register와 명령 추적

5.1 PC Register란

Program Counter Register — JVM이 현재 실행 중인 바이트코드 명령의 위치를 가리키는 포인터.

add() 메서드의 바이트코드:
  0: iload_1        ◄── PC가 여기를 가리키는 중
  1: iload_2
  2: iadd
  3: istore_3
  ...

명령이 실행될 때마다 PC가 다음 명령으로 이동.
분기(if, goto, return)는 PC를 다른 위치로 점프시킴.

5.2 왜 스레드별로 있어야 하는가

Thread 1                  Thread 2
PC = method1+5            PC = method2+12
   ↓                         ↓
각자 다른 명령을 동시에 실행

각 스레드는 자기만의 실행 위치를 가진다.
공유하면 → 모두가 같은 위치에서 같은 명령을 실행 → 동시성 의미 없음.

Stack과 PC Register는 같이 스레드별. 한 세트.

5.3 메서드 호출 시 PC의 동작

public void caller() {
    int x = 10;       // PC=0~2
    int y = callee(); // PC=3~5 (invoke)
    use(y);           // PC=6~...
}

public int callee() {
    return 42;
}

호출 흐름:

1. caller PC=3 — invokestatic callee
2. Frame Data에 Return Address = 6 저장 (호출자의 다음 PC)
3. callee 프레임 push, PC = callee의 0번
4. callee 실행 완료
5. ireturn — 반환값을 caller의 Operand Stack에 push
6. callee 프레임 pop
7. caller로 복귀 — PC를 Return Address (=6) 로 복원
8. caller PC=6부터 계속

Frame Data의 Return Address가 정확히 이 메커니즘을 위해 존재.

5.4 Native 메서드와 PC

public native void method();   // JNI

Native 메서드 실행 중에는 PC Register는 undefined.
→ JVM 바이트코드가 아닌 C/C++ 코드를 실행 중이라, JVM의 PC가 의미 없음.
→ JNI 호출 정보는 Native Method Stack에 따로 저장.

5.5 PC를 직접 보는 도구

운영 중인 JVM의 현재 명령 위치는 thread dump에서 확인 가능.

jstack <PID>

출력:

"http-nio-8080-exec-1" #25 daemon prio=5 tid=0x... nid=0x... runnable
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        ...

→ 각 줄이 스택의 한 프레임.
→ "현재 PC가 어느 메서드의 어느 줄을 가리키는가" 의 시각화.


6️⃣ 메서드 호출과 반환의 전체 흐름

6.1 호출 7단계

caller() {
    ...
    int result = callee(10, 20);
    ...
}
  1. 인자 준비: caller의 Operand Stack에 인자 push

    Caller Stack: [10, 20]
  2. invoke 명령 실행: invokestatic, invokevirtual

  3. 새 프레임 생성: callee의 프레임 push

    Stack:
      ┌────────────┐
      │ callee     │ ◄── 새 프레임
      ├────────────┤
      │ caller     │
      └────────────┘
  4. 인자 이동: caller의 Operand Stack top → callee의 LVA

    callee LVA: [10, 20]
    Caller Stack: []
  5. Return Address 저장: callee의 Frame Data에 caller의 다음 PC 저장

  6. PC 이동: PC를 callee의 첫 명령으로

  7. callee 실행 시작

6.2 반환 5단계

callee의 마지막: ireturn
  1. 반환값 결정: callee의 Operand Stack top이 반환값

    callee Stack: [30] ← 반환할 값
  2. 프레임 pop: callee 프레임 제거

  3. PC 복원: Frame Data의 Return Address → PC

  4. 반환값 push: caller의 Operand Stack에 30 push

    Caller Stack: [30]
  5. caller 계속: PC가 가리키는 곳부터 실행 (예: istore_X)

6.3 void 반환

public void log(String msg) {
    System.out.println(msg);
    return;     // 또는 생략
}

→ 반환값 없음. 그냥 프레임 pop + PC 복원.
→ caller의 Operand Stack에는 아무것도 push되지 않음.

6.4 예외 발생 시

public int divide(int a, int b) {
    return a / b;    // b=0이면 ArithmeticException
}
  1. ArithmeticException 객체 생성 (Heap)
  2. divide의 예외 테이블 검사 → 매칭되는 catch 없음
  3. 프레임 pop
  4. caller로 예외 전파
  5. caller의 예외 테이블 검사 → ...
  6. 반복하다 main까지 도달 → JVM 종료

예외도 Stack을 거꾸로 거슬러 올라간다.
→ 이게 Stack Trace의 정체.


7️⃣ 멀티스레드와 Stack — 컨텍스트 스위칭

7.1 스레드별 Stack의 진정한 의미

Process (JVM)
  ├─ Thread A ──── Stack A + PC A
  ├─ Thread B ──── Stack B + PC B
  └─ Thread C ──── Stack C + PC C

         모두 공유:
           Heap + Method Area

각 스레드는:

  • 자기 Stack (지역 변수 안 섞임)
  • 자기 PC Register (실행 위치 따로)
  • 자기 Native Method Stack

공유하는 것:

  • Heap (객체)
  • Method Area (클래스 정보)

→ 그래서 멀티스레드 환경에서 Heap의 객체 접근만 동기화하면 됨.

7.2 컨텍스트 스위칭

OS가 한 스레드에서 다른 스레드로 CPU를 넘기는 작업.

Thread A 실행 중:
  CPU 레지스터 ← Thread A의 PC, SP, 작업 데이터

CPU 컨텍스트 스위칭:
  1. Thread A의 CPU 상태를 메모리에 저장
  2. Thread B의 저장된 CPU 상태를 레지스터에 복원
  3. Thread B 실행 시작

각 스레드가 자기 Stack과 PC를 따로 가지고 있어야 컨텍스트 스위칭이 가능.

7.3 ILIC에서 마주칠 멀티스레드 시나리오

@RestController
public class ShipmentController {

    private final ShipmentService service;   // 모든 스레드 공유

    @GetMapping("/{id}")
    public ShipmentResponse get(@PathVariable Long id) {
        // 이 메서드는 톰캣의 worker thread 풀의 어떤 스레드가 실행
        // - 각 요청마다 새 Stack 프레임
        // - id는 그 스레드의 Stack에만 존재
        return ShipmentResponse.from(service.findById(id));
    }
}
HTTP 요청 100개 동시 → 톰캣 worker 100개 (또는 NIO + 적은 worker)

각 worker:
  자기 Stack
    - id 변수
    - shipment 지역 변수
  자기 PC
    - 지금 어디 실행 중인지

공유:
  ShipmentController 인스턴스 (Heap)
  ShipmentService 인스턴스 (Heap)
  Shipment 클래스 정보 (Method Area)

7.4 ThreadLocal — 스레드별 데이터의 또 다른 보관소

private static final ThreadLocal<String> userContext = new ThreadLocal<>();

// 스레드 A에서
userContext.set("user-001");

// 같은 스레드 A의 다른 메서드에서
userContext.get();   // "user-001"

// 스레드 B에서
userContext.get();   // null  ← 다른 스레드는 못 봄

→ Stack의 지역 변수는 메서드 종료 시 사라짐.
→ ThreadLocal은 스레드 단위로 살아남는 변수.
→ Spring Security, 트랜잭션 등에서 활용.

⚠️ 누수 위험: ThreadLocal은 Heap에 저장됨. Worker thread가 재사용되는 톰캣 환경에서 remove() 안 하면 메모리 누수.

try {
    userContext.set(user);
    process();
} finally {
    userContext.remove();    // ★ 반드시
}

→ 1주차 try-with-resources 패턴과 연결.


8️⃣ ILIC 실무 + 디버깅

8.1 Stack Trace 읽기 — 가장 기본

java.lang.NullPointerException
    at com.ilic.shipment.ShipmentService.calculate(ShipmentService.java:45)
    at com.ilic.shipment.ShipmentController.get(ShipmentController.java:23)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

읽는 법:

  • 위에서부터 아래로 = 호출 역순 (현재 → 호출한 곳)
  • 가장 위의 줄 = 예외가 실제 발생한 위치 (45번 줄)
  • 그 아래 줄 = 그 메서드를 호출한 곳 (23번 줄)
  • at = "여기에서 호출했음"

각 줄은 스택 프레임 1개. 예외 발생 시 위에서 아래로 Stack을 dump한 것.

8.2 Caused by와 Suppressed (1주차 Unit 7.1 연결)

java.lang.RuntimeException: 운임 계산 실패
    at com.ilic.shipment.FareCalculator.compute(FareCalculator.java:78)
    at com.ilic.shipment.ShipmentService.calculate(ShipmentService.java:45)
    ...
Caused by: java.sql.SQLException: ORA-01017
    at oracle.jdbc.driver.OraclePreparedStatement.execute(...)
    ...
Suppressed: java.io.IOException: connection reset
    at ...
  • Caused by: 원인 예외 (예외 체이닝)
  • Suppressed: try-with-resources의 close에서 발생한 예외

→ 운영에서 Caused by를 추적해 진짜 원인 찾기.

8.3 운영 서버 Thread Dump

# 1. JVM PID 확인
jps -l

# 2. Thread dump 생성
jstack <PID> > thread-dump.txt

# 3. 시점을 여러 번 떠서 비교 (3~5초 간격, 3번 정도)
jstack <PID> > dump1.txt
sleep 3
jstack <PID> > dump2.txt
sleep 3
jstack <PID> > dump3.txt

8.4 Thread Dump 분석 — Deadlock 탐지

"Thread-A" #11 prio=5 ... waiting for monitor entry [...]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.ilic.LockService.methodA(LockService.java:30)
        - waiting to lock <0x000000076c123000> (a java.lang.Object)
        - locked <0x000000076c456000> (a java.lang.Object)

"Thread-B" #12 prio=5 ... waiting for monitor entry [...]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.ilic.LockService.methodB(LockService.java:50)
        - waiting to lock <0x000000076c456000> (a java.lang.Object)
        - locked <0x000000076c123000> (a java.lang.Object)

해독:

  • Thread-A는 0x...456000을 잡고, 0x...123000을 기다림
  • Thread-B는 0x...123000을 잡고, 0x...456000을 기다림
  • 데드락

jstack은 데드락을 자동 감지해서 출력 끝에 표시:

Found one Java-level deadlock:
=============================
"Thread-A": waiting to lock monitor 0x00..., which is held by "Thread-B"
"Thread-B": waiting to lock monitor 0x00..., which is held by "Thread-A"

8.5 BLOCKED / WAITING 스레드 찾기

운영 장애 흔한 원인:

  • 다수 스레드가 같은 모니터에서 BLOCKED → 락 경합
  • DB Connection Pool 대기 → WAITING (on object monitor)
  • HTTP 외부 호출 무한 대기 → RUNNABLE이지만 socketRead 멈춤

ILIC 시나리오:

  • 톰캣 worker 200개가 모두 BLOCKED → 추가 요청 처리 불가 → 서비스 다운
  • Thread dump를 떠서 어디서 막혔는지 추적

8.6 StackOverflowError 원인 분석

public BigDecimal calculateRecursive(int depth) {
    return calculateRecursive(depth + 1);   // 무한 재귀
}

발생:

Exception in thread "main" java.lang.StackOverflowError
    at ShipmentService.calculateRecursive(ShipmentService.java:10)
    at ShipmentService.calculateRecursive(ShipmentService.java:10)
    at ShipmentService.calculateRecursive(ShipmentService.java:10)
    ...
    (수천 줄)

해결 옵션:
1. 로직 수정: 종료 조건 추가
2. 반복문 변환: 재귀를 while/for로
3. -Xss 증가: 깊은 재귀가 필요한 경우 (최후의 수단)

java -Xss2m MyApp    # 스택 크기 2MB로

8.7 운영 가이드 — JVM 옵션 모음

# Stack 크기
-Xss512k                        # 기본 약 512KB ~ 1MB

# Thread Dump를 시그널로 받기 (Unix)
kill -3 <PID>                   # JVM이 stderr에 thread dump 출력

# Heap dump on OOM (1.2 Unit에서 본 것)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/

# Async-profiler 같은 도구도 Stack 정보 활용

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
스택 프레임의 3가지 구성 요소?Local Variable Array · Operand Stack · Frame Data
JVM이 스택 머신이라는 의미?모든 산술 연산이 Operand Stack의 push/pop으로 일어남
int sum = a + b의 바이트코드?iload_1 → iload_2 → iadd → istore_3
Local Variable Array의 [0]은 보통 무엇?인스턴스 메서드의 this (static이면 첫 매개변수)
PC Register가 스레드별인 이유?각 스레드가 독립적으로 다른 명령을 실행 중. 공유하면 동시성 의미 없음
메서드 호출 시 Return Address는 어디에?새 프레임의 Frame Data에 저장 (호출자의 다음 PC)
메서드 반환 시 반환값 전달?callee의 Operand Stack top → caller의 Operand Stack에 push
StackOverflowError vs OutOfMemoryError 차이?전자는 Stack 영역, 후자는 Heap/Metaspace 등 다른 영역
Thread Dump를 어떻게 읽나?위→아래가 호출 역순. 최상단이 현재 실행 위치
long이 LVA에서 2슬롯 차지하는 이유?64비트라 32비트 슬롯 2개 사용

9.2 자기 점검 체크리스트

기본 이해

  • 스택 프레임의 3가지 구성 요소를 안다
  • LVA가 배열 인덱스로 변수에 접근함을 안다
  • Operand Stack의 동작(push/pop으로 연산)을 안다
  • PC Register의 역할과 스레드별 분리 이유를 안다
  • 메서드 호출과 반환의 전체 흐름을 그릴 수 있다

실전 적용

  • int sum = a + b의 바이트코드를 추적할 수 있다
  • Stack Trace의 각 줄이 무엇을 의미하는지 안다
  • Thread Dump를 만들고 BLOCKED 스레드를 찾을 수 있다
  • Deadlock 패턴을 thread dump에서 식별할 수 있다
  • StackOverflowError 원인을 코드와 dump로 분석할 수 있다

면접 대비 — 5분 답변

  • 스택 프레임 구조와 동작
  • JVM이 스택 머신인 이유
  • PC Register와 컨텍스트 스위칭
  • 예외와 Stack Trace의 관계
  • ThreadLocal과 Stack의 차이

🎯 핵심 요약 — 3줄 정리

1. 스택 프레임 = LVA + Operand Stack + Frame Data

  • LVA: 지역 변수와 매개변수 (배열 인덱스로 접근)
  • Operand Stack: 연산 임시 공간 (JVM은 스택 머신)
  • Frame Data: Return Address 등 메타 정보

2. JVM의 모든 연산은 Operand Stack 위에서

  • 변수 → 스택 push → 연산 → 결과 스택 → 변수 저장
  • 메서드 호출도 인자를 스택에 쌓고 invoke
  • 반환값은 호출자 스택에 push

3. 멀티스레드 = 스레드별 Stack + PC

  • 각 스레드는 자기 Stack과 PC를 가짐
  • Heap과 Method Area만 공유 → 동기화 대상
  • Thread Dump = 모든 스레드의 Stack 스냅샷

📚 다음으로...

Unit 1.5 — Heap과 객체-Metadata 연결

이번 Unit에서 메서드가 Stack에서 어떻게 실행되는지 봤다면, 다음은 Heap의 객체가 자기 클래스 메서드를 어떻게 찾아가는지 정밀하게 본다.

  • Object Header의 Class Pointer
  • VMT (Virtual Method Table)
  • 메서드 호출 시 dispatch 메커니즘
  • invokevirtual vs invokespecial vs invokestatic vs invokeinterface
  • 다형성의 메모리 구현 (1주차 Unit 2.4의 내부)

2주차 진행 상황

  • ✅ 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 (메서드 실행 메커니즘)
  • ⏭ Phase 3 (바이트코드와 상수 풀) ★ 정점

Phase 3로 가는 길 — 점점 가까워지는 바이트코드

이번 Unit에서 iload_1, iadd, istore_3 같은 바이트코드를 자연스럽게 봤다.
Phase 3에서는 이런 명령들을 javap -c -v로 직접 디컴파일하면서 상수 풀과 함께 읽는다.

이번 Unit의 추상화 수준:
  iload_1 → "LVA[1] 값을 스택에 푸시"

Phase 3에서 보게 될 수준:
  3: invokespecial #1  // Method java/lang/Object."<init>":()V
  → #1이 상수 풀의 1번 항목 → "java/lang/Object"의 "<init>"의 시그니처 "()V"
profile
Software Developer

0개의 댓글