2주차 Unit 1.2 — 변수별 저장 위치 (Stack / Heap / Method Area)

Psj·2026년 5월 12일

F-lab

목록 보기
51/238

Unit 1.2 — 변수별 저장 위치 (Stack / Heap / Method Area)

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


📌 학습 목표

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

  • Shipment s = new Shipment() 한 줄로 메모리 어디에 무엇이 생기는가?
  • 참조 변수(s)객체 본체 는 각각 어디에 저장되는가?
  • 클래스 변수가 "모든 객체가 공유"되는 진짜 이유는?
  • Stack은 왜 스레드별로 따로 있는가?
  • 객체가 자기 클래스의 메서드를 어떻게 찾아가는가?
  • 운영 서버에서 OOM이 났을 때 어느 영역을 의심해야 하는가?

🎯 핵심 한 문장

자바의 메모리는 3개의 본진(Stack · Heap · Method Area)으로 분할되고, 변수 종류에 따라 정확히 하나의 본진에 배치된다.
그리고 이들은 참조(reference) 라는 화살표로 서로를 가리키며 협력한다.

비유 — 음식점의 3개 공간

메모리 영역음식점 비유특징
Method Area주방 + 메뉴판가게에 1개. 모든 손님이 같은 메뉴
Heap손님 테이블 위 음식손님(객체)마다 자기 음식. 정리되면 사라짐
Stack종업원의 주문서 패드종업원(스레드)마다 자기 패드. 주문 끝나면 버림

손님이 카르보나라를 시키면:

  • 주방의 카르보나라 레시피(Method Area)는 그대로 공유
  • 손님 테이블의 카르보나라 한 접시(Heap의 객체)는 손님마다 따로
  • 종업원의 "3번 테이블, 카르보나라, 16:30" 메모(Stack)는 종업원마다 따로

🧭 9개 섹션 로드맵

1. JVM 메모리 5분할 (1주차 복습)
2. Stack Area              — 스레드별 작업대
3. Heap Area               — 객체의 거대한 풀
4. Method Area             — 클래스의 청사진 저장소
5. 코드 추적                — new Member()의 메모리 변화
6. 참조 vs 객체 본체        — 화살표가 연결하는 세 영역
7. ILIC 실무 시각화         — 멀티스레드 환경의 메모리 그림
8. 흔한 실수 + 디버깅        — heap dump · thread dump 읽기
9. 면접 질문 + 자기 점검

1️⃣ JVM 메모리 5분할 (1주차 복습)

1.1 5개 영역 다시 보기

┌────────────────────────────────────────────────────┐
│                  JVM Memory                        │
│                                                    │
│  ┌──────────────┐   ┌──────────────────────────┐  │
│  │ Method Area  │   │         Heap             │  │
│  │              │   │                          │  │
│  │ • 클래스 정보  │   │   • 모든 new 객체         │  │
│  │ • static 변수 │   │   • 인스턴스 변수         │  │
│  │ • 상수 풀     │   │   • 배열                 │  │
│  │              │   │                          │  │
│  │ (모든 스레드   │   │ (모든 스레드 공유,         │  │
│  │  공유)        │   │  GC 대상)                │  │
│  └──────────────┘   └──────────────────────────┘  │
│                                                    │
│  ┌──────────────┐   ┌─────┐   ┌──────────────┐   │
│  │  PC Register │   │Stack│   │ Native Method│   │
│  │              │   │     │   │    Stack     │   │
│  │ (스레드별)    │   │(스레 │   │ (스레드별)    │   │
│  └──────────────┘   │드별) │   └──────────────┘   │
│                     └─────┘                       │
└────────────────────────────────────────────────────┘

1.2 공유 영역 vs 스레드별 영역

영역공유?변수 종류
Method Area모든 스레드 공유클래스 변수 (static)
Heap모든 스레드 공유인스턴스 변수 (객체 안)
Stack스레드별지역 변수 + 매개변수
PC Register스레드별현재 실행 명령 위치
Native Method Stack스레드별JNI 호출

→ 핵심은 3개: Stack · Heap · Method Area.
나머지 2개는 보조.

1.3 이번 Unit에서 집중할 것

변수 3종류  ↔  메모리 3영역  ↔  스레드 공유 여부

지역 변수    ─────►   Stack     →  스레드별
인스턴스 변수 ─────►   Heap      →  공유 (객체를 공유하면)
클래스 변수   ─────►   Method Area  →  무조건 공유

2️⃣ Stack Area — 스레드별 작업대

2.1 구조 — 스택 프레임(Stack Frame)

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

    public void run() {
        int x = 10;
        int y = 20;
        int result = add(x, y);
    }
}

run()add()를 호출하는 순간의 Stack:

Stack (LIFO, 위가 top)
┌──────────────────────────────┐
│  add() 프레임                  │  ← 현재 실행 중
│    매개변수: a=10, b=20        │
│    지역변수: sum=30           │
├──────────────────────────────┤
│  run() 프레임                  │  ← 잠시 멈춤
│    지역변수: x=10, y=20       │
│              result=??       │
├──────────────────────────────┤
│  main() 프레임                 │
│    ...                       │
└──────────────────────────────┘

add()가 끝나면:

  • add() 프레임이 통째로 제거
  • 반환값(30)이 run()result로 들어감
  • run() 프레임이 다시 top

메서드 호출 = 프레임 push, 메서드 종료 = 프레임 pop.

2.2 무엇이 Stack 프레임에 들어가는가

스택 프레임 1개의 내용물:
┌──────────────────────────────┐
│ 1. 매개변수 (Parameters)       │  ← 호출자가 넣어줌
│ 2. 지역 변수 (Local Variables) │  ← 메서드 안에서 선언
│ 3. 피연산자 스택 (Operand Stack)│  ← 계산용 임시 공간
│ 4. 반환 주소 (Return Address)   │  ← 끝나면 어디로 돌아갈지
└──────────────────────────────┘

핵심: 매개변수와 지역 변수가 같은 영역(스택 프레임)에 있다.
→ Unit 1.1에서 "매개변수는 지역 변수의 특수형"이라 한 이유.

2.3 왜 스레드별로 따로인가?

Thread 1                 Thread 2
┌──────────┐            ┌──────────┐
│ add()    │            │ process()│
│   sum=30 │            │   id=42  │
├──────────┤            ├──────────┤
│ run()    │            │ main()   │
└──────────┘            └──────────┘
   Stack 1                 Stack 2
   (Thread 1 전용)         (Thread 2 전용)

이유:

  • 각 스레드는 독립적으로 메서드를 호출하고 있음
  • 만약 스택을 공유하면 → 어느 프레임이 누구 것인지 알 수 없음
  • 동시 호출 시 데이터 충돌

지역 변수는 자동으로 스레드 안전 (스택이 분리되어 있으므로).
→ 함수형 코드, Stream, 람다가 동시성에 강한 근본 이유.

2.4 Stack 크기와 StackOverflowError

public void recurse() {
    recurse();    // 자기 자신을 호출
}

각 호출마다 새 프레임 push → 스택이 가득 차면:

Exception in thread "main" java.lang.StackOverflowError

기본 크기: 보통 512KB ~ 1MB (JVM 옵션 -Xss 로 조절).

java -Xss512k MyApp    # 스택 크기 512KB로 설정

→ 깊은 재귀가 필요하면 -Xss 늘리거나, 반복문으로 변환.


3️⃣ Heap Area — 객체의 거대한 풀

3.1 무엇이 Heap에 들어가는가

Shipment s = new Shipment("BL-001");
List<Cargo> cargoes = new ArrayList<>();
int[] weights = new int[100];

new라는 키워드가 들어간 모든 것은 Heap.

  • 객체 본체
  • 객체 안의 인스턴스 변수
  • 배열
  • 박싱된 객체 (Integer, Long, ...)
  • String 객체 본체 (단, 리터럴은 String Pool — Unit 1.6)

3.2 객체의 메모리 레이아웃

public class Shipment {
    private Long id;            // 8 bytes (reference)
    private String blNo;        // 8 bytes (reference)
    private int weight;         // 4 bytes
    private LocalDate eta;      // 8 bytes (reference)
}

Heap에 만들어지는 모습:

┌─────────────────────────────────┐
│  Shipment 객체 @0x7f4a2c01      │
├─────────────────────────────────┤
│  [Object Header]                │  ← 12~16 bytes (Mark word + Class pointer)
│   ├─ Mark word (GC 정보, lock)   │
│   └─ Class pointer ────────────┐│  ← Method Area의 Shipment 클래스 가리킴
├─────────────────────────────────┤
│  id          : 0x7f4a3d12 ──┐  ││  ← Long 객체의 주소
│  blNo        : 0x7f4a4e23 ─┐│  ││  ← String 객체의 주소
│  weight      : 1500         ││  ││  ← int는 값 직접 저장
│  eta         : 0x7f4a5f34 ┐│││  ││  ← LocalDate 객체의 주소
├─────────────────────────────────┤
│  [Padding]                  │││  ││  ← 8 바이트 정렬
└─────────────────────────────┼┼┼──┼┘
                              │││  │
                              ▼▼▼  ▼
                      (Heap의 다른 객체들 또는 Method Area)

핵심:

  • 기본 타입(int, long, boolean 등)은 값을 직접 저장
  • 참조 타입(객체)은 주소(레퍼런스) 만 저장 → 실제 객체는 또 다른 Heap 위치에
  • 모든 객체는 Object Header를 가짐 (Class pointer 포함)

3.3 Class Pointer — 모든 객체의 본적

Heap                       Method Area
┌──────────────┐
│ Shipment #1  │
│  Class ptr ──┼───┐
└──────────────┘   │
                   ▼
┌──────────────┐   ┌────────────────────┐
│ Shipment #2  │   │  Shipment 클래스    │
│  Class ptr ──┼──►│  • 필드 정보        │
└──────────────┘   │  • 메서드 바이트코드 │
                   │  • static 변수      │
┌──────────────┐   └────────────────────┘
│ Shipment #3  │           ▲
│  Class ptr ──┼───────────┘
└──────────────┘

객체 100개를 만들면, 메서드 바이트코드는 1개.
→ 각 객체는 자기 클래스 metadata를 가리키는 포인터 하나만 가짐.

⚠️ 다음 Unit 1.3에서 Method Area를 3개 존으로 분해할 때, 이 그림이 더 정밀해진다.

3.4 Heap은 GC의 영역

Stack은 메서드 종료 시 자동 정리 → GC 불필요.
Method Area는 클래스 단위라 거의 안 사라짐 → GC 거의 없음.
Heap만이 GC의 진짜 무대.

1주차 GC 학습 내용 복습:
  Young Generation (Eden + Survivor)
       ↓ Promotion
  Old Generation
       ↓ Full GC

→ Heap의 객체가 GC된다는 의미.

3.5 모든 스레드가 공유

Thread 1                 Thread 2
   ↓                        ↓
┌───────────────────────────────┐
│         Heap (공유)            │
│                               │
│  Shipment #1, #2, #3, ...     │
└───────────────────────────────┘

→ 여러 스레드가 같은 객체에 접근 가능 → 동시성 문제의 발생지.
synchronized, volatile, AtomicLong, ConcurrentHashMap 같은 도구가 필요한 이유.


4️⃣ Method Area — 클래스의 청사진 저장소

4.1 무엇이 들어가는가

public class Shipment {
    public static final String COMPANY = "ILIC";   // ← 클래스 변수
    private static int totalCount = 0;             // ← 클래스 변수

    private String blNo;                            // ← (선언 정보만)

    public BigDecimal calculate() {                 // ← 메서드 바이트코드
        // ...
    }
}

Method Area에 저장되는 것:

┌─────────────────────────────────────────┐
│  Method Area                            │
│                                         │
│  ┌───────────────────────────────────┐ │
│  │  Shipment 클래스 정보              │ │
│  │  • 클래스명                        │ │
│  │  • 부모 클래스 (Object)            │ │
│  │  • 인터페이스 목록                  │ │
│  │  • 필드 선언 (이름, 타입, 접근)      │ │
│  │  • 메서드 선언 + 바이트코드         │ │
│  │  • 상수 풀 (Constant Pool)        │ │
│  │  • 클래스 변수 (static):            │ │
│  │     COMPANY = "ILIC"              │ │
│  │     totalCount = 0                │ │
│  └───────────────────────────────────┘ │
└─────────────────────────────────────────┘

4.2 클래스 변수가 "공유"되는 진짜 이유

Shipment.totalCount++;        // Method Area에 단 1개인 변수를 변경
new Shipment().getCount();    // 같은 단 1개를 읽음
new Shipment().getCount();    // 또 같은 단 1개를 읽음

→ 모든 객체가 Method Area의 같은 위치를 참조 → 자동으로 공유.
→ 별도 동기화 로직 없이도 모든 객체가 같은 값을 봄.
→ 단점: 동시 쓰기 시 race condition (Unit 1.1 흔한 실수 2번).

4.3 Method Area는 GC의 영역인가?

대부분 아니다.

  • 클래스는 한 번 로딩되면 거의 안 사라짐
  • 다만 클래스 언로딩은 가능 (조건: ClassLoader가 GC됨)
  • 동적 로딩(Reflection, 동적 프록시)이 많으면 Method Area가 가득 차서 OOM
java.lang.OutOfMemoryError: Metaspace

→ Java 8+에선 Method Area의 실제 구현이 Metaspace (네이티브 메모리).

4.4 Method Area는 모든 스레드 공유

Thread 1                 Thread 2
   ↓                        ↓
┌───────────────────────────────┐
│      Method Area (공유)         │
│                               │
│  Shipment 클래스               │
│  Cargo 클래스                  │
│  String 클래스                 │
└───────────────────────────────┘

→ 클래스 로딩은 단 한 번. 이후 모든 스레드가 그 정보를 본다.
→ 클래스 변수도 자동 공유.

4.5 PermGen → Metaspace (Java 8의 변화)

Java 7 이전:
  Heap의 일부 = PermGen
  (크기 고정, OOM 자주 발생)

Java 8 이후:
  Heap 밖 = Metaspace (네이티브 메모리)
  (크기 자동 확장, OOM 줄어듦)

면접 단골: "Java 8에서 PermGen이 사라졌다는데 무엇이 바뀐 건가?"
→ Method Area의 구현체가 Heap에서 네이티브 메모리(Metaspace)로 옮겨졌다.


5️⃣ 코드 추적 — new Shipment()의 메모리 변화

5.1 추적할 코드

public class App {
    public static void main(String[] args) {
        Shipment s = new Shipment("BL-001");
        BigDecimal fare = s.calculate();
        System.out.println(fare);
    }
}

5.2 단계별 메모리 변화

시점 0 — JVM 시작 (App 클래스 로딩 직후)

Method Area:
  ┌──────────────────────┐
  │ App 클래스            │
  │   main() 바이트코드   │
  └──────────────────────┘
  ┌──────────────────────┐
  │ Shipment 클래스       │
  │   calculate() 코드   │
  │   COMPANY = "ILIC"  │
  └──────────────────────┘

Stack: (비어있음)
Heap: (비어있음)

시점 1 — main() 시작

Method Area: (그대로)

Stack:
  ┌──────────────────────┐
  │ main() 프레임          │  ← push
  │   args = (어떤 배열)   │
  │   s = ?               │
  │   fare = ?            │
  └──────────────────────┘

Heap: (비어있음)

시점 2 — new Shipment("BL-001") 실행 중 (생성자 호출)

Method Area: (그대로)

Stack:
  ┌──────────────────────┐
  │ <init>() 프레임        │  ← push (생성자)
  │   this = 0x7f4a2c01   │
  │   blNo (매개변수) = "BL-001"
  ├──────────────────────┤
  │ main() 프레임          │
  └──────────────────────┘

Heap:
  ┌──────────────────────┐
  │ Shipment @0x7f4a2c01  │  ← 메모리 할당됨, 생성자 실행 중
  │   Class ptr → Shipment │
  │   blNo = null         │  (아직 미초기화)
  └──────────────────────┘

시점 3 — 생성자 종료, s에 참조 저장

Method Area: (그대로)

Stack:
  ┌──────────────────────┐
  │ main() 프레임          │
  │   args = ...          │
  │   s = 0x7f4a2c01 ─────┼──┐  ← 참조 저장
  │   fare = ?            │  │
  └──────────────────────┘  │
                            │
Heap:                       │
  ┌──────────────────────┐  │
  │ Shipment @0x7f4a2c01  │◄─┘
  │   blNo = "BL-001"     │
  └──────────────────────┘

s는 Stack에, 객체 본체는 Heap에. s의 값은 객체의 주소(참조).

시점 4 — s.calculate() 호출 중

Stack:
  ┌──────────────────────┐
  │ calculate() 프레임     │  ← push
  │   this = 0x7f4a2c01   │  ← 호출자(s)의 참조가 this로
  │   baseFare = 100000   │
  ├──────────────────────┤
  │ main() 프레임          │
  │   s = 0x7f4a2c01      │
  └──────────────────────┘

Heap:
  ┌──────────────────────┐
  │ Shipment @0x7f4a2c01  │
  └──────────────────────┘

핵심: 인스턴스 메서드 호출 시 this 매개변수가 자동으로 전달됨.
→ 이게 객체와 메서드를 연결하는 메커니즘.

시점 5 — calculate() 종료

Stack:
  ┌──────────────────────┐
  │ main() 프레임          │
  │   s = 0x7f4a2c01      │
  │   fare = 0x7f4a3d12 ──┼──┐  ← BigDecimal 객체 참조
  └──────────────────────┘  │
                            │
Heap:                       │
  ┌──────────────────────┐  │
  │ Shipment @0x7f4a2c01  │  │
  ├──────────────────────┤  │
  │ BigDecimal @0x7f4a3d12│◄─┘
  │   값: 250000          │
  └──────────────────────┘

시점 6 — main() 종료

Stack: (비어있음)

Heap:
  ┌──────────────────────┐
  │ Shipment @0x7f4a2c01  │  ← 참조 끊김. GC 대상
  ├──────────────────────┤
  │ BigDecimal @0x7f4a3d12│  ← 참조 끊김. GC 대상
  └──────────────────────┘

sfare 변수가 사라짐 → 객체들도 더 이상 참조 안 됨 → 다음 GC 때 회수.


6️⃣ 참조 vs 객체 본체 — 화살표가 연결하는 세 영역

6.1 핵심 통찰

지역 변수(Stack)  ─참조─►  객체(Heap)  ─Class ptr─►  클래스(Method Area)

세 영역이 화살표로 연결되어 협력한다.

6.2 같은 객체를 가리키는 두 변수

Shipment a = new Shipment("BL-001");
Shipment b = a;

메모리 상태:

Stack:
  a = 0x7f4a2c01 ──┐
  b = 0x7f4a2c01 ──┤
                   │
Heap:              ▼
  ┌──────────────────────┐
  │ Shipment @0x7f4a2c01  │  ← 단 1개. a와 b가 같이 가리킴
  │   blNo = "BL-001"     │
  └──────────────────────┘
b.setBlNo("CHANGED");
a.getBlNo();   // "CHANGED"  ← 같은 객체이므로

참조 복사는 객체 복사가 아니다. 화살표만 하나 더 생긴 것.

6.3 메서드 매개변수도 같은 방식

void modify(Shipment s) {
    s.setBlNo("BY_METHOD");
}

Shipment original = new Shipment("BL-001");
modify(original);
original.getBlNo();   // "BY_METHOD"
호출 시점:
  Stack:
    main: original = 0x7f4a2c01 ──┐
    modify: s = 0x7f4a2c01 ────────┤
                                   │
  Heap:                            ▼
    Shipment @0x7f4a2c01 → blNo = "BY_METHOD"

soriginal다른 Stack 위치의 다른 변수지만, 같은 Heap 객체를 가리킨다.
→ 1주차 Unit 4.2 (Pass by Value)의 실체.

6.4 객체 안의 객체

public class Shipment {
    private String blNo;
    private List<Cargo> cargoes;
}
Shipment s = new Shipment();
s.setBlNo("BL-001");
s.setCargoes(new ArrayList<>());
s.getCargoes().add(new Cargo("C1"));

메모리 상태:

Stack:
  s = 0x7f4a2c01 ───┐
                    │
Heap:               ▼
  ┌────────────────────────┐
  │ Shipment @0x7f4a2c01   │
  │   blNo = 0x7f4a3d12 ───┼──► String "BL-001" @0x7f4a3d12
  │   cargoes = 0x7f4a4e23 ┼──┐
  └────────────────────────┘  │
                              ▼
  ┌────────────────────────┐
  │ ArrayList @0x7f4a4e23   │
  │   elementData[0] ──────┼──► Cargo "C1" @0x7f4a5f34
  │   size = 1              │
  └────────────────────────┘

→ Heap 안에서 객체끼리 또 화살표로 연결.
→ 이게 객체 그래프(object graph). 직렬화(Unit 7.4)가 이 그래프를 따라 순회.


7️⃣ ILIC 실무 시각화 — 멀티스레드 환경

7.1 시나리오 — HTTP 요청 3개 동시 처리

@RestController
public class ShipmentController {

    private final ShipmentService service;   // 인스턴스 변수 (Spring 싱글톤)

    @GetMapping("/shipments/{id}")
    public ShipmentResponse get(@PathVariable Long id) {
        Shipment shipment = service.findById(id);   // 지역 변수
        return ShipmentResponse.from(shipment);
    }
}

3개 요청이 동시에:

  • GET /shipments/1 (스레드 A)
  • GET /shipments/2 (스레드 B)
  • GET /shipments/3 (스레드 C)

7.2 메모리 그림

┌────────────────────────────────────────────────────┐
│  Method Area (모두 공유)                            │
│  • ShipmentController 클래스                       │
│  • ShipmentService 클래스                          │
│  • Shipment 클래스                                  │
└────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────┐
│  Heap (모두 공유)                                   │
│  ┌──────────────────────────┐                      │
│  │ ShipmentController (싱글톤) │  ← 단 1개. 모든 요청 공유
│  │   service ────────────────┼──┐                  │
│  └──────────────────────────┘  │                  │
│                                ▼                  │
│  ┌──────────────────────────┐                      │
│  │ ShipmentService (싱글톤)   │  ← 단 1개. 모든 요청 공유
│  └──────────────────────────┘                      │
│                                                    │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐           │
│  │ Shipment │ │ Shipment │ │ Shipment │           │
│  │ id=1     │ │ id=2     │ │ id=3     │  ← 요청마다 다름
│  └──────────┘ └──────────┘ └──────────┘           │
└────────────────────────────────────────────────────┘

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Stack A    │  │  Stack B    │  │  Stack C    │
│  get(1) {   │  │  get(2) {   │  │  get(3) {   │
│    id=1     │  │    id=2     │  │    id=3     │
│    shipment │  │    shipment │  │    shipment │
│    →@id=1   │  │    →@id=2   │  │    →@id=3   │
│  }          │  │  }          │  │  }          │
└─────────────┘  └─────────────┘  └─────────────┘
  Thread A         Thread B         Thread C

핵심 관찰:
1. ShipmentController, ShipmentService 인스턴스는 단 1개 — Spring 싱글톤. 모든 스레드 공유.
2. 각 요청의 Shipment 데이터는 따로 — Heap에 3개 별도 객체.
3. 각 스레드의 Stack은 완전 분리id, shipment 지역 변수 안 섞임.

→ 그래서 Service의 인스턴스 변수는 final + 불변(주입된 의존성)이어야 안전.
→ 가변 인스턴스 변수를 두면 → 3개 스레드가 동시에 변경 → race condition.

7.3 운영 사고 시나리오 — 잘못된 카운터

@Service
public class ShipmentService {

    private int requestCount = 0;     // ❌ 인스턴스 변수, 가변

    public Shipment findById(Long id) {
        requestCount++;               // ❌ 3개 스레드가 동시에 ++
        return repository.findById(id).orElseThrow();
    }
}
실제 동작 (3개 스레드 동시 ++):
  T0:  requestCount = 0
  T1:  A 읽음 → 0,  B 읽음 → 0,  C 읽음 → 0
  T2:  A는 1로 씀, B는 1로 씀, C는 1로 씀
  T3:  최종 값 = 1   ❌ (3이어야 함)

2개의 요청이 사라짐. 통계가 부정확.

해결:

private final AtomicInteger requestCount = new AtomicInteger(0);

public Shipment findById(Long id) {
    requestCount.incrementAndGet();
    // ...
}

7.4 OOM 분석 시 어디를 보나

메시지영역원인
OutOfMemoryError: Java heap spaceHeap객체 너무 많음, 누수, 캐시 폭발
OutOfMemoryError: MetaspaceMethod Area클래스 너무 많음 (동적 프록시, ClassLoader 누수)
StackOverflowErrorStack깊은 재귀, 무한 호출
OutOfMemoryError: Direct buffer memoryHeap 밖ByteBuffer.allocateDirect 누수 (1주차 NIO Unit)

Heap 누수 의심 시:

# Heap dump 생성
jmap -dump:format=b,file=heap.hprof <PID>

# 분석 도구: Eclipse MAT, IntelliJ Profiler

Metaspace 누수:

  • 보통 ClassLoader 누수 (Tomcat redeploy, 동적 클래스 생성 라이브러리)
  • -XX:MaxMetaspaceSize로 한계 설정 + 모니터링

8️⃣ 흔한 실수 + 디버깅

실수 1 — 참조 복사를 객체 복사라고 착각

Shipment a = new Shipment("BL-001");
Shipment b = a;
b.setBlNo("CHANGED");
a.getBlNo();   // "CHANGED" — 같은 객체

// 진짜 복사가 필요하면:
Shipment c = new Shipment(a.getBlNo());    // 새 객체
// 또는 builder, copy constructor, Cloneable 등

실수 2 — 메서드 안에서 매개변수를 재할당해도 호출자에 영향 없음

public void clear(Shipment s) {
    s = null;     // s는 메서드의 지역 복사본. 호출자의 변수와 무관
}

Shipment original = new Shipment();
clear(original);
original;   // ✓ null 아님. 그대로

→ 1주차 Unit 4.2 핵심. 매개변수도 지역 변수.

실수 3 — Heap 누수 (long-living reference)

public class ShipmentCache {
    private static final Map<Long, Shipment> cache = new HashMap<>();
    //                ↑ static → Method Area에 영원히 살아있음
    //                          → cache가 들고 있는 Shipment들도 GC 안 됨

    public static void put(Long id, Shipment s) {
        cache.put(id, s);   // 계속 쌓임 → OOM
    }
}

→ 1주차 직렬화 Unit + Unit 1.1에서 본 패턴.

실수 4 — Stack 너무 깊은 재귀

public int factorial(int n) {
    return n == 0 ? 1 : n * factorial(n - 1);
}

factorial(100_000);    // ❌ StackOverflowError

→ 큰 입력에는 반복문으로 변환하거나, 꼬리 재귀(Java는 미지원).

실수 5 — 변수 스코프 오해로 NPE

public Shipment find(Long id) {
    Shipment result;       // 지역 변수, 미초기화

    if (id != null) {
        result = repository.findById(id).orElseThrow();
    }
    // id가 null이면? result 초기화 안 됨 → 컴파일 에러
    return result;
}

// ✅ 명시적 초기화
Shipment result = null;

실수 6 — Heap dump 분석 시 String 누수 간과

운영 OOM의 흔한 범인:

  • String 객체가 가장 많음 (보통 30% 이상)
  • 진짜 누수일 수도, 정상일 수도
  • "String을 누가 들고 있는가"를 추적 → GC Roots까지의 경로(Path)

디버깅 도구 요약

# 1) JVM 옵션 (운영 권장)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heap.hprof

# 2) 살아있는 JVM에서 dump
jmap -dump:live,format=b,file=heap.hprof <PID>

# 3) Thread dump (Stack 상태)
jstack <PID>

# 4) 메모리 영역별 사용량
jmap -heap <PID>

# 5) GC 통계
jstat -gc <PID> 1000   # 1초마다

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

9.1 면접 단골 질문 매핑

Q핵심 답변
Shipment s = new Shipment() 에서 s와 객체는 어디에?s(참조)는 Stack, 객체 본체는 Heap
클래스 변수가 모든 객체에 공유되는 이유?Method Area에 단 1개 있고, 모든 객체가 같은 위치를 참조하므로
Stack이 스레드별로 있는 이유?각 스레드가 독립적으로 메서드를 호출하므로. 공유 시 충돌
Heap이 GC의 무대인 이유?Stack은 자동 정리, Method Area는 거의 안 사라짐. 객체 회수는 Heap에서만
인스턴스 메서드 호출 시 객체 인식?this 참조가 매개변수로 자동 전달됨
객체가 자기 클래스 메서드를 어떻게 찾나?객체 헤더의 Class Pointer → Method Area의 클래스 정보
Java 8 PermGen → Metaspace 변화?Method Area 구현이 Heap에서 네이티브 메모리로 이동
참조 변수와 객체 본체의 관계?Stack의 참조가 Heap의 객체를 가리킴. 화살표 관계
같은 객체를 두 변수가 가리키면?화살표가 2개. 한 쪽으로 변경하면 다른 쪽도 같이 보임
OOM의 종류별 원인은?Heap space(객체 누수), Metaspace(클래스 누수), StackOverflow(재귀)

9.2 자기 점검 체크리스트

기본 이해

  • Stack · Heap · Method Area 각각 무엇이 저장되는지 안다
  • 스레드별 영역과 공유 영역을 구분할 수 있다
  • 참조 변수와 객체 본체가 다른 곳에 있음을 안다
  • 클래스 변수가 공유되는 메커니즘을 설명할 수 있다
  • Stack Frame의 구성 요소(매개변수·지역·피연산자·반환주소)를 안다

실전 적용

  • new Shipment() 한 줄의 메모리 변화를 단계별로 그릴 수 있다
  • 같은 객체를 두 변수가 가리키는 코드의 동작을 예측할 수 있다
  • 멀티스레드 환경에서 어느 변수가 공유되고 어느 변수가 분리되는지 안다
  • OOM 메시지를 보고 어느 영역의 문제인지 추정할 수 있다
  • heap dump · thread dump 명령을 사용할 수 있다

면접 대비 — 5분 답변

  • 메모리 3영역의 역할과 차이
  • 참조 vs 객체 본체의 분리
  • 스레드 안전성과 메모리 영역의 관계
  • Class Pointer 메커니즘
  • PermGen → Metaspace 변화 (Java 8)

🎯 핵심 요약 — 3줄 정리

1. 변수 종류 = 저장 영역

  • 지역 변수 → Stack (스레드별, 메서드 종료 시 사라짐)
  • 인스턴스 변수 → Heap (객체 안, GC 대상)
  • 클래스 변수 → Method Area (클래스에 1개, JVM 종료까지)

2. 세 영역은 화살표로 연결된다

  • Stack의 참조 → Heap의 객체 → Method Area의 클래스 정보
  • 참조 변수와 객체 본체는 다른 메모리 위치
  • 객체 헤더의 Class Pointer가 객체와 클래스를 연결

3. 멀티스레드 안전성은 영역으로 결정된다

  • Stack: 스레드별 → 자동 안전
  • Heap: 공유 → 객체 동기화 필요
  • Method Area: 공유 → 가변 static은 항상 위험

📚 다음으로...

Unit 1.3 — Method Area의 3개 존 (★ 2주차 핵심)

이번 Unit에서 Method Area를 큰 그림으로 봤다면, 다음은 3개 존으로 분해한다.

  • Class Metadata Zone: 클래스명, 필드 선언, 메서드 시그니처
  • Static Zone: static 메서드 바이트코드, static 필드
  • Non-Static Zone: 인스턴스 메서드 바이트코드

main()이 왜 Static Zone에 있는지, 그게 왜 객체 없이 호출 가능한지 정확히 이해.
→ Phase 2(메서드 실행 메커니즘), Phase 3(바이트코드)의 토대.

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 (바이트코드와 상수 풀) ★ 정점
profile
Software Developer

0개의 댓글