F-LAB JAVA · 2주차 · Phase 1 · 자바 변수 ↔ 메모리 영역의 매핑
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Shipment s = new Shipment() 한 줄로 메모리 어디에 무엇이 생기는가?s) 와 객체 본체 는 각각 어디에 저장되는가?자바의 메모리는 3개의 본진(Stack · Heap · Method Area)으로 분할되고, 변수 종류에 따라 정확히 하나의 본진에 배치된다.
그리고 이들은 참조(reference) 라는 화살표로 서로를 가리키며 협력한다.
| 메모리 영역 | 음식점 비유 | 특징 |
|---|---|---|
| Method Area | 주방 + 메뉴판 | 가게에 1개. 모든 손님이 같은 메뉴 |
| Heap | 손님 테이블 위 음식 | 손님(객체)마다 자기 음식. 정리되면 사라짐 |
| Stack | 종업원의 주문서 패드 | 종업원(스레드)마다 자기 패드. 주문 끝나면 버림 |
손님이 카르보나라를 시키면:
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. 면접 질문 + 자기 점검
┌────────────────────────────────────────────────────┐
│ JVM Memory │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Method Area │ │ Heap │ │
│ │ │ │ │ │
│ │ • 클래스 정보 │ │ • 모든 new 객체 │ │
│ │ • static 변수 │ │ • 인스턴스 변수 │ │
│ │ • 상수 풀 │ │ • 배열 │ │
│ │ │ │ │ │
│ │ (모든 스레드 │ │ (모든 스레드 공유, │ │
│ │ 공유) │ │ GC 대상) │ │
│ └──────────────┘ └──────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌─────┐ ┌──────────────┐ │
│ │ PC Register │ │Stack│ │ Native Method│ │
│ │ │ │ │ │ Stack │ │
│ │ (스레드별) │ │(스레 │ │ (스레드별) │ │
│ └──────────────┘ │드별) │ └──────────────┘ │
│ └─────┘ │
└────────────────────────────────────────────────────┘
| 영역 | 공유? | 변수 종류 |
|---|---|---|
| Method Area | 모든 스레드 공유 | 클래스 변수 (static) |
| Heap | 모든 스레드 공유 | 인스턴스 변수 (객체 안) |
| Stack | 스레드별 | 지역 변수 + 매개변수 |
| PC Register | 스레드별 | 현재 실행 명령 위치 |
| Native Method Stack | 스레드별 | JNI 호출 |
→ 핵심은 3개: Stack · Heap · Method Area.
나머지 2개는 보조.
변수 3종류 ↔ 메모리 3영역 ↔ 스레드 공유 여부
지역 변수 ─────► Stack → 스레드별
인스턴스 변수 ─────► Heap → 공유 (객체를 공유하면)
클래스 변수 ─────► Method Area → 무조건 공유
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.
스택 프레임 1개의 내용물:
┌──────────────────────────────┐
│ 1. 매개변수 (Parameters) │ ← 호출자가 넣어줌
│ 2. 지역 변수 (Local Variables) │ ← 메서드 안에서 선언
│ 3. 피연산자 스택 (Operand Stack)│ ← 계산용 임시 공간
│ 4. 반환 주소 (Return Address) │ ← 끝나면 어디로 돌아갈지
└──────────────────────────────┘
핵심: 매개변수와 지역 변수가 같은 영역(스택 프레임)에 있다.
→ Unit 1.1에서 "매개변수는 지역 변수의 특수형"이라 한 이유.
Thread 1 Thread 2
┌──────────┐ ┌──────────┐
│ add() │ │ process()│
│ sum=30 │ │ id=42 │
├──────────┤ ├──────────┤
│ run() │ │ main() │
└──────────┘ └──────────┘
Stack 1 Stack 2
(Thread 1 전용) (Thread 2 전용)
이유:
→ 지역 변수는 자동으로 스레드 안전 (스택이 분리되어 있으므로).
→ 함수형 코드, Stream, 람다가 동시성에 강한 근본 이유.
public void recurse() {
recurse(); // 자기 자신을 호출
}
각 호출마다 새 프레임 push → 스택이 가득 차면:
Exception in thread "main" java.lang.StackOverflowError
기본 크기: 보통 512KB ~ 1MB (JVM 옵션 -Xss 로 조절).
java -Xss512k MyApp # 스택 크기 512KB로 설정
→ 깊은 재귀가 필요하면 -Xss 늘리거나, 반복문으로 변환.
Shipment s = new Shipment("BL-001");
List<Cargo> cargoes = new ArrayList<>();
int[] weights = new int[100];
new라는 키워드가 들어간 모든 것은 Heap.
Integer, Long, ...)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 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개 존으로 분해할 때, 이 그림이 더 정밀해진다.
Stack은 메서드 종료 시 자동 정리 → GC 불필요.
Method Area는 클래스 단위라 거의 안 사라짐 → GC 거의 없음.
Heap만이 GC의 진짜 무대.
1주차 GC 학습 내용 복습:
Young Generation (Eden + Survivor)
↓ Promotion
Old Generation
↓ Full GC
→ Heap의 객체가 GC된다는 의미.
Thread 1 Thread 2
↓ ↓
┌───────────────────────────────┐
│ Heap (공유) │
│ │
│ Shipment #1, #2, #3, ... │
└───────────────────────────────┘
→ 여러 스레드가 같은 객체에 접근 가능 → 동시성 문제의 발생지.
→ synchronized, volatile, AtomicLong, ConcurrentHashMap 같은 도구가 필요한 이유.
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 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Shipment.totalCount++; // Method Area에 단 1개인 변수를 변경
new Shipment().getCount(); // 같은 단 1개를 읽음
new Shipment().getCount(); // 또 같은 단 1개를 읽음
→ 모든 객체가 Method Area의 같은 위치를 참조 → 자동으로 공유.
→ 별도 동기화 로직 없이도 모든 객체가 같은 값을 봄.
→ 단점: 동시 쓰기 시 race condition (Unit 1.1 흔한 실수 2번).
대부분 아니다.
java.lang.OutOfMemoryError: Metaspace
→ Java 8+에선 Method Area의 실제 구현이 Metaspace (네이티브 메모리).
Thread 1 Thread 2
↓ ↓
┌───────────────────────────────┐
│ Method Area (공유) │
│ │
│ Shipment 클래스 │
│ Cargo 클래스 │
│ String 클래스 │
└───────────────────────────────┘
→ 클래스 로딩은 단 한 번. 이후 모든 스레드가 그 정보를 본다.
→ 클래스 변수도 자동 공유.
Java 7 이전:
Heap의 일부 = PermGen
(크기 고정, OOM 자주 발생)
Java 8 이후:
Heap 밖 = Metaspace (네이티브 메모리)
(크기 자동 확장, OOM 줄어듦)
면접 단골: "Java 8에서 PermGen이 사라졌다는데 무엇이 바뀐 건가?"
→ Method Area의 구현체가 Heap에서 네이티브 메모리(Metaspace)로 옮겨졌다.
new Shipment()의 메모리 변화public class App {
public static void main(String[] args) {
Shipment s = new Shipment("BL-001");
BigDecimal fare = s.calculate();
System.out.println(fare);
}
}
Method Area:
┌──────────────────────┐
│ App 클래스 │
│ main() 바이트코드 │
└──────────────────────┘
┌──────────────────────┐
│ Shipment 클래스 │
│ calculate() 코드 │
│ COMPANY = "ILIC" │
└──────────────────────┘
Stack: (비어있음)
Heap: (비어있음)
main() 시작Method Area: (그대로)
Stack:
┌──────────────────────┐
│ main() 프레임 │ ← push
│ args = (어떤 배열) │
│ s = ? │
│ fare = ? │
└──────────────────────┘
Heap: (비어있음)
new Shipment("BL-001") 실행 중 (생성자 호출)Method Area: (그대로)
Stack:
┌──────────────────────┐
│ <init>() 프레임 │ ← push (생성자)
│ this = 0x7f4a2c01 │
│ blNo (매개변수) = "BL-001"
├──────────────────────┤
│ main() 프레임 │
└──────────────────────┘
Heap:
┌──────────────────────┐
│ Shipment @0x7f4a2c01 │ ← 메모리 할당됨, 생성자 실행 중
│ Class ptr → Shipment │
│ blNo = null │ (아직 미초기화)
└──────────────────────┘
s에 참조 저장Method Area: (그대로)
Stack:
┌──────────────────────┐
│ main() 프레임 │
│ args = ... │
│ s = 0x7f4a2c01 ─────┼──┐ ← 참조 저장
│ fare = ? │ │
└──────────────────────┘ │
│
Heap: │
┌──────────────────────┐ │
│ Shipment @0x7f4a2c01 │◄─┘
│ blNo = "BL-001" │
└──────────────────────┘
→ s는 Stack에, 객체 본체는 Heap에. s의 값은 객체의 주소(참조).
s.calculate() 호출 중Stack:
┌──────────────────────┐
│ calculate() 프레임 │ ← push
│ this = 0x7f4a2c01 │ ← 호출자(s)의 참조가 this로
│ baseFare = 100000 │
├──────────────────────┤
│ main() 프레임 │
│ s = 0x7f4a2c01 │
└──────────────────────┘
Heap:
┌──────────────────────┐
│ Shipment @0x7f4a2c01 │
└──────────────────────┘
핵심: 인스턴스 메서드 호출 시 this 매개변수가 자동으로 전달됨.
→ 이게 객체와 메서드를 연결하는 메커니즘.
calculate() 종료Stack:
┌──────────────────────┐
│ main() 프레임 │
│ s = 0x7f4a2c01 │
│ fare = 0x7f4a3d12 ──┼──┐ ← BigDecimal 객체 참조
└──────────────────────┘ │
│
Heap: │
┌──────────────────────┐ │
│ Shipment @0x7f4a2c01 │ │
├──────────────────────┤ │
│ BigDecimal @0x7f4a3d12│◄─┘
│ 값: 250000 │
└──────────────────────┘
main() 종료Stack: (비어있음)
Heap:
┌──────────────────────┐
│ Shipment @0x7f4a2c01 │ ← 참조 끊김. GC 대상
├──────────────────────┤
│ BigDecimal @0x7f4a3d12│ ← 참조 끊김. GC 대상
└──────────────────────┘
→ s와 fare 변수가 사라짐 → 객체들도 더 이상 참조 안 됨 → 다음 GC 때 회수.
지역 변수(Stack) ─참조─► 객체(Heap) ─Class ptr─► 클래스(Method Area)
세 영역이 화살표로 연결되어 협력한다.
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" ← 같은 객체이므로
→ 참조 복사는 객체 복사가 아니다. 화살표만 하나 더 생긴 것.
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"
→ s와 original은 다른 Stack 위치의 다른 변수지만, 같은 Heap 객체를 가리킨다.
→ 1주차 Unit 4.2 (Pass by Value)의 실체.
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)가 이 그래프를 따라 순회.
@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)┌────────────────────────────────────────────────────┐
│ 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.
@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();
// ...
}
| 메시지 | 영역 | 원인 |
|---|---|---|
OutOfMemoryError: Java heap space | Heap | 객체 너무 많음, 누수, 캐시 폭발 |
OutOfMemoryError: Metaspace | Method Area | 클래스 너무 많음 (동적 프록시, ClassLoader 누수) |
StackOverflowError | Stack | 깊은 재귀, 무한 호출 |
OutOfMemoryError: Direct buffer memory | Heap 밖 | ByteBuffer.allocateDirect 누수 (1주차 NIO Unit) |
Heap 누수 의심 시:
# Heap dump 생성
jmap -dump:format=b,file=heap.hprof <PID>
# 분석 도구: Eclipse MAT, IntelliJ Profiler
Metaspace 누수:
-XX:MaxMetaspaceSize로 한계 설정 + 모니터링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 등
public void clear(Shipment s) {
s = null; // s는 메서드의 지역 복사본. 호출자의 변수와 무관
}
Shipment original = new Shipment();
clear(original);
original; // ✓ null 아님. 그대로
→ 1주차 Unit 4.2 핵심. 매개변수도 지역 변수.
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에서 본 패턴.
public int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
factorial(100_000); // ❌ StackOverflowError
→ 큰 입력에는 반복문으로 변환하거나, 꼬리 재귀(Java는 미지원).
public Shipment find(Long id) {
Shipment result; // 지역 변수, 미초기화
if (id != null) {
result = repository.findById(id).orElseThrow();
}
// id가 null이면? result 초기화 안 됨 → 컴파일 에러
return result;
}
// ✅ 명시적 초기화
Shipment result = null;
운영 OOM의 흔한 범인:
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초마다
| 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(재귀) |
new Shipment() 한 줄의 메모리 변화를 단계별로 그릴 수 있다1. 변수 종류 = 저장 영역
2. 세 영역은 화살표로 연결된다
3. 멀티스레드 안전성은 영역으로 결정된다
이번 Unit에서 Method Area를 큰 그림으로 봤다면, 다음은 3개 존으로 분해한다.
→ main()이 왜 Static Zone에 있는지, 그게 왜 객체 없이 호출 가능한지 정확히 이해.
→ Phase 2(메서드 실행 메커니즘), Phase 3(바이트코드)의 토대.