아키텍처 클래스 로딩 런타임 영역 힙 · 객체 수명 GC 알고리즘 튜닝 · 진단 📝 문제풀이
📋 목차 심화 CH 02 ▶
⚙️ 심화 CHAPTER 01 · 5년차 트랙

JVM은 내 코드를 어떻게 실행하고, 메모리를 어떻게 정리할까?

문법은 이미 손에 익었을 거예요. 이번 트랙은 "왜 이렇게 동작하는가"를 파고듭니다. 클래스가 로딩되는 순서, 메모리가 나뉘는 방식, GC가 멈춤(STW)을 만드는 이유 — 장애 대응과 성능 튜닝, 그리고 시니어 면접의 단골 주제예요.

🎯 이 장을 끝내면
🏗️
아키텍처
JVM을 세 덩어리로 보기
클래스 로더 · 런타임 데이터 영역 · 실행 엔진

javac가 만든 바이트코드(.class)는 그 자체로는 CPU가 못 읽어요. JVM이 이걸 읽어들이고(클래스 로더), 메모리에 배치하고(런타임 데이터 영역), 기계어로 바꿔 실행(실행 엔진)합니다. 이 세 덩어리의 협업이 JVM의 전부라고 봐도 무방해요.

📥
클래스 로더
.class를 찾아 로딩·링킹·초기화
🗂️
런타임 데이터 영역
힙·메타스페이스·스택·PC
실행 엔진
인터프리터·JIT·GC
⚡ 실행 엔진 안에는 3가지가 산다 ① 인터프리터 — 바이트코드를 한 줄씩 해석해 실행. 시작은 빠르지만 반복 실행은 느려요.
② JIT 컴파일러(C1/C2)자주 실행되는(hot) 코드를 기계어로 미리 컴파일해 캐시. 계층 컴파일(Tiered Compilation)로 C1(빠른 컴파일)→C2(고도 최적화)로 승격돼요.
③ GC — 더 이상 참조되지 않는 객체의 메모리를 회수. 이 장의 절반이 여기예요. 그래서 자바는 "인터프리터 언어냐 컴파일 언어냐"로 딱 잘라 말하기 어려워요. 둘을 섞은 하이브리드(AOT는 별개)라고 보는 게 정확합니다.
💡
JIT의 대표 최적화 두 가지. 인라이닝(작은 메서드 호출을 본문으로 펼침)과 이스케이프 분석(객체가 메서드 밖으로 새지 않으면 스택 할당·스칼라 치환으로 힙 할당 자체를 없앰). "왜 마이크로벤치마크가 실제와 다르냐"의 상당수가 JIT 워밍업 때문이에요 — 그래서 JMH를 씁니다.
📥
클래스 로딩
로딩 · 링킹 · 초기화, 그리고 부모 위임
static 초기화 순서 버그의 근원이 여기 있어요.

클래스는 처음 능동적으로 사용되는 순간에 로딩돼요(지연 로딩). 과정은 크게 세 단계예요.

단계하는 일핵심 포인트
1. 로딩(Loading).class 바이트스트림을 읽어 메서드 영역에 Class 객체 생성어떤 로더가 읽었는지가 클래스 정체성의 일부
2. 링킹(Linking)검증(바이트코드 무결성) → 준비(static 필드에 기본값 할당) → 해석(심볼릭 참조 → 직접 참조)준비 단계의 static int x는 아직 0
3. 초기화(Init)static 초기화 블록·static 필드 실제 값 대입 실행여기서 x=10이 진짜 적용. 스레드 안전하게 1회 보장
⚠️
"준비"와 "초기화"를 헷갈리면 버그가 됩니다. 준비 단계에서 모든 static 필드는 타입의 기본값(0, false, null)을 먼저 받아요. 개발자가 쓴 = 10 같은 대입은 초기화 단계에서야 실행됩니다. static 필드가 서로를 참조하며 초기화되면 선언 순서에 따라 "아직 기본값인 상태"를 읽는 일이 생겨요.
class Config {
    static int a = b + 1;   // 초기화 시점: b는 아직 0 → a = 1
    static int b = 2;       // 그 다음 b = 2
}
// Config.a == 1, Config.b == 2  (선언 순서의 함정)
🧬 부모 위임 모델(Parent Delegation) 클래스 로더는 요청을 받으면 먼저 부모 로더에게 위임하고, 부모가 못 찾을 때만 자기가 로딩해요. 계층은 Bootstrap → Platform → Application 순이에요(Java 9부터 Extension이 Platform으로 바뀜). 이 모델 덕에 java.lang.String 같은 핵심 클래스를 사용자가 위조해도 항상 Bootstrap이 로딩한 진짜가 쓰여요 — 보안·일관성의 핵심 장치입니다. (반대로 톰캣·OSGi는 이 규칙을 의도적으로 깨서 앱별 격리를 만들어요.)
🧭
같은 이름, 다른 클래스. JVM에서 클래스의 정체성은 (패키지+클래스명) + 로딩한 클래스로더예요. 그래서 서로 다른 로더가 읽은 동일 FQCN은 ClassCastException을 낼 수 있어요 — 플러그인·핫리로딩 환경의 단골 이슈죠.
🖼️ 그림으로 보기 — 클래스로더 부모 위임 모델
Bootstrap 로더 핵심 java.* (최상위) Platform 로더 JDK 모듈 Application 로더 클래스패스 (내 코드) ① 위로 위임 부모에게 먼저 요청 ② 아래로 로드 부모가 못 찾으면 핵심 클래스는 항상 Bootstrap이 로드 → 사용자 위조 방지
🗂️
런타임 영역
무엇이 공유되고, 무엇이 스레드별인가
이 구분이 동시성·OOM 진단의 기초예요.
영역범위담는 것
힙(Heap)🌐 모든 스레드 공유모든 객체 인스턴스·배열. GC의 주 무대. (String Pool도 Java 7부터 힙에 있음)
메서드 영역 / 메타스페이스🌐 공유클래스 메타데이터·런타임 상수 풀·static 필드. Java 8부터 네이티브 메모리의 Metaspace
JVM 스택🧵 스레드별메서드 호출마다 프레임(지역변수·피연산자 스택). 깊으면 StackOverflowError
PC 레지스터🧵 스레드별현재 실행 중인 바이트코드 명령 주소
네이티브 메서드 스택🧵 스레드별JNI 등 네이티브 코드 호출용 스택
⚠️
PermGen은 이제 없어요(Java 8~). 예전 PermGen(힙 내부, 고정 크기)은 클래스가 많으면 OutOfMemoryError: PermGen space를 자주 냈어요. Java 8에서 Metaspace로 대체됐고, 이건 힙이 아니라 네이티브 메모리에 있어 기본적으로 자동 확장돼요. 대신 무한정 새는 클래스로더가 있으면 OutOfMemoryError: Metaspace가 납니다(-XX:MaxMetaspaceSize로 상한).
Heap Metaspace JVM Stack PC Register Native Stack String Pool
🖼️ 그림으로 보기 — 공유 영역 vs 스레드별 영역
🌐 공유 — 모든 스레드가 함께 힙(Heap) 모든 객체 인스턴스·배열 · GC의 주 무대 String Pool (Java 7+) 메서드 영역 / 메타스페이스 클래스 메타데이터·상수풀·static Java 8+ 네이티브 메모리 🧵 스레드별 — 각자 하나씩 스레드 A JVM 스택 프레임·지역변수 PC 레지스터 네이티브 스택 스레드 B JVM 스택 프레임·지역변수 PC 레지스터 네이티브 스택
♻️
힙 · 객체 수명
Young에서 태어나 Old로 늙는다
세대 가설(Weak Generational Hypothesis)이 GC 설계를 지배해요.

대부분의 객체는 금방 죽는다 — 이 경험칙(약한 세대 가설)에 맞춰 힙을 YoungOld로 나눠요. Young은 Eden + Survivor(S0/S1)로 구성됩니다.

🥚
Eden
새 객체가 태어나는 곳
🐣
Survivor S0/S1
Minor GC 생존자 임시 보관·age 증가
🦉
Old(Tenured)
age 임계값 넘으면 승격

흐름은 이래요. 새 객체는 Eden에 할당되고, Minor GC 때 살아남으면 Survivor로 옮겨지며 age가 1 증가해요. age가 tenuring threshold(기본 최대 15)를 넘으면 Old로 승격(promotion)됩니다. Old가 차면 더 무거운 Major/Full GC가 돌아요.

💡
TLAB(Thread-Local Allocation Buffer). Eden 할당은 사실 스레드마다 전용 버퍼를 미리 떼어주고 그 안에서 포인터만 밀어 할당해요(bump-the-pointer). 덕분에 대부분의 객체 할당이 락 없이 이뤄져 매우 빠릅니다. 할당이 병목처럼 보인다면 TLAB·이스케이프 분석부터 의심하세요.
🔎 왜 "대부분 금방 죽는다"에 베팅할까 Young 영역은 살아남은 소수만 복사(copying)하고 나머지는 통째로 비워요. 죽은 객체가 많을수록 복사 비용이 작아 Minor GC가 싸집니다. 반대로 오래 사는 객체가 Young에 많으면 복사가 잦아져 비효율적이라 Old로 보내는 거예요. 그래서 "짧게 쓰고 버리는 임시 객체"는 GC 친화적이고, 어중간하게 오래 사는 캐시성 객체가 오히려 GC를 힘들게 합니다(중간 수명 문제).
🖼️ 그림으로 보기 — 힙 세대 구조와 객체 승격
Young Generation Eden 새 객체 태어남 (TLAB 할당) Survivor S0 Survivor S1 Minor GC 생존자를 S0↔S1로 복사하며 age +1 Old (Tenured) 오래 살아남은 객체 승격 age가 tenuring threshold(기본 15) 초과 → Old 승격 · Old가 차면 Major/Full GC
🧹
GC 알고리즘
STW를 어떻게 줄이느냐의 역사
컬렉터마다 "처리량 vs 지연시간" 트레이드오프가 달라요.

GC의 근본 딜레마는 STW(Stop-The-World)예요. 정확히 회수하려면 애플리케이션 스레드를 잠깐 멈춰야 하는데, 이 멈춤을 얼마나 짧게, 얼마나 자주 만드느냐로 컬렉터가 갈립니다.

컬렉터성격언제
Serial단일 스레드, 작은 힙·단순 환경클라이언트·작은 컨테이너
Parallel(Throughput)여러 스레드로 STW 처리, 처리량 최우선배치처럼 지연보다 총량이 중요할 때
CMS (제거됨)동시 수집으로 지연↓. Java 9 deprecated, Java 14 제거이제 쓰지 않음(G1/ZGC로 이전)
G1Region 단위 분할, 목표 정지시간(MaxGCPauseMillis) 지향. Java 9부터 기본범용 기본값. 수 GB 힙
ZGC밀리초 이하 STW, 수 TB 힙까지. Java 15 정식, Java 21 세대별 ZGC대용량·초저지연
Shenandoah동시 압축(concurrent compaction)으로 저지연저지연(주로 OpenJDK/Corretto)
⚠️
"동시(concurrent) GC = STW 0"이 아니에요. G1·ZGC도 초기 마킹·최종 마킹이나 evacuation 일부 단계에서 짧은 STW가 있어요. 다만 오래 걸리는 마킹·정리 작업을 애플리케이션과 동시에 돌려 멈춤 시간을 줄이는 거죠. "정지가 없다"가 아니라 "정지가 짧다"가 정확합니다.
📉 Minor vs Major vs Full GC Minor GC — Young 영역만 청소. 빈번하지만 짧아요.
Major GC — Old 영역 청소.
Full GC — 힙 전체(+메타스페이스 정리 유발 가능). 가장 길고 비쌈. 잦은 Full GC는 대개 메모리 누수·과도한 승격·힙 부족 신호예요. 운영 경보의 핵심 지표는 "Full GC 빈도와 pause 시간"입니다. 여기가 튀면 지연·타임아웃이 따라와요.
🔧
튜닝 · 진단
숫자를 보고 판단하기
추측 말고 측정 — 플래그·OOM 종류·도구.
플래그의미
-Xms / -Xmx초기 / 최대 크기. 운영에선 보통 둘을 같게 설정(리사이즈로 인한 변동 제거)
-Xss스레드 스택 크기(깊은 재귀·많은 스레드에 영향)
-XX:MaxMetaspaceSize메타스페이스 상한(클래스 누수 방어)
-XX:+UseG1GC / -XX:+UseZGC컬렉터 선택
-XX:MaxGCPauseMillisG1 목표 정지시간(힌트)
-XX:+HeapDumpOnOutOfMemoryErrorOOM 시 힙 덤프 자동 저장(사후 분석용)
🚨 OutOfMemoryError는 한 종류가 아니에요 Java heap space — 진짜 힙 부족/누수. 힙 덤프를 MAT로 분석.
GC overhead limit exceeded — GC는 도는데 회수가 거의 안 됨(누수 직전 신호).
Metaspace — 클래스/클래스로더 누수(동적 프록시·리로딩 남발).
Direct buffer memory / Unable to create native thread — 힙 바깥(네이티브·스레드 수) 문제. 힙만 키우면 안 됨. OOM 메시지의 뒷부분을 반드시 읽으세요. 원인 영역이 다르면 처방(힙↑ vs 스레드 수↓ vs 누수 수정)도 완전히 달라집니다.
💡
진단 도구 최소 세트. jcmd <pid> GC.heap_info·VM.native_memory, jstat -gcutil <pid> 1s(세대별 사용률·GC 횟수 실시간), GC 로그(-Xlog:gc*)를 GCeasy 같은 도구로 시각화, 힙 덤프는 jmap -dumpEclipse MAT로 지배자 트리(dominator tree) 분석. "추측하지 말고 덤프를 떠라"가 원칙이에요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
시니어 관점으로 점검
면접·장애대응에서 실제로 묻는 결의 문제예요. 틀려도 바로 해설이 나옵니다.
🧪
개념을 "안다"와 "설명할 수 있다"는 달라요. 네 유형(객관식 · O/X · 빈칸 · 코드결과)으로 점검해보세요.
심화 CH 02 🧵 동시성과 메모리 모델 — happens-before, volatile, 락, CompletableFuture, 가상 스레드 (제작 예정)