F-lab Java 1주차 / Phase 5 / Unit 5.2 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 5.1 (GC의 기본 개념과 약한 세대 가설)
다음 Unit: 5.3 — GC 알고리즘 4가지이 Unit의 의미: 약한 세대 가설의 구체적 구현.
"Heap이 어떻게 나뉘어 있고, 객체가 어떤 일생을 사는가" 의 정확한 이해.
Minor GC vs Major GC vs Full GC 의 차이 — 면접 단골.
큰 병원을 상상해보세요. 환자가 들어오면 어떻게 분류할까요?
1층 — 응급실 (Eden):
2층 — 일반 병동 (Survivor):
3층 — 장기 입원 병동 (Old):
병원의 운영 원리:
→ 이게 자바 Heap의 세대 구조.
상황: 아파트가 3개 있다고 상상.
아파트 A (Eden):
아파트 B & C (Survivor 0, Survivor 1):
아파트 D (Old):
왜 Survivor가 2개?:
→ 이게 Eden + Survivor 0/1 의 운영 원리.
"자바 Heap은 객체의 수명에 따라 영역을 나누고, 다른 GC 전략을 적용한다."
3가지 핵심 영역:
비유 정리:
| 비유 요소 | Heap 적용 |
|---|---|
| 응급실 | Eden |
| 일반 병동 (2개) | Survivor 0, Survivor 1 |
| 장기 입원 병동 | Old Generation |
| 응급실 청소 | Minor GC |
| 장기 병동 청소 | Major GC |
| 전체 청소 | Full GC |
Java 초기 (1.0~1.4) 의 GC 는 Heap 전체를 한 번에 청소했습니다.
문제:
원인 분석:
약한 세대 가설 (Unit 5.1) 활용:
"대부분 객체는 단명, 살아남은 것은 장수"
아이디어:
실측 효과:
처음에는 Young Generation 이 단순했습니다:
문제:
해결 — Survivor 영역 ⭐ :
Eden ──→ Survivor (몇 회) ──→ Old
(필터링)
효과:
왜 Survivor 가 2개?
답: Copying GC 를 활용하기 위해.
Copying GC:
Survivor 0 (From) ↔ Survivor 1 (To) 운영:
Round 1:
[Eden + Survivor 0] → Survivor 1
(살아남은 객체 복사)
Eden 비움, Survivor 0 비움
Round 2:
[Eden + Survivor 1] → Survivor 0
(역방향)
Eden 비움, Survivor 1 비움
... 반복 ...
핵심:
→ 이게 Eden + Survivor 0/1 의 정확한 운영 원리.
문제: Old → Young 참조가 있을 수 있음.
public class OldObject {
private YoungObject young; // Old → Young 참조
}
Minor GC 시 Young 만 보면 이 참조를 놓칠 수 있음.
해결 — Card Table (Unit 5.1 미리보기):
→ 약한 세대 가설 덕분에 Old → Young 참조는 적음 → 효율적.
"Heap의 세대 구조는 약한 세대 가설을 정확히 반영한 정교한 시스템이다."
Eden + Survivor 0/1 + Old 로 분리, Copying GC 로 자동 압축, Card Table 로 세대 간 참조 추적, Promotion 으로 진짜 장수 객체 분리. 모든 결정이 "가설을 어떻게 효율적으로 활용할까" 의 답.
이 구조를 정확히 이해하면 GC 알고리즘 (Unit 5.3), GC 종류 (Unit 5.4), 운영 환경 GC 튜닝까지 자연스럽게 흡수.
세대 구조를 모르면 GC 관련 문제 분석이 어렵습니다.
운영 환경 GC 로그:
GC(0) Pause Young 25M->2M(64M) 5ms
GC(1) Pause Young 27M->3M(64M) 6ms
GC(2) Pause Young 25M->2M(64M) 5ms
... 1초마다 발생 ...
세대 구조 모르면:
세대 구조 알면:
-Xmn4g (Young 영역 4GB)-XX:NewRatio 조정GC 로그:
GC(0) Pause Young 25M->20M(64M) 5ms ← Old로 많이 Promotion
GC(1) Pause Young 30M->25M(64M) 6ms
...
GC(50) Pause Full 60M->50M(64M) 200ms ← Full GC 발생
세대 구조 모르면:
세대 구조 알면:
GC 로그:
[GC pause (G1 Evacuation Pause) (young) (to-space exhausted)]
의미: Survivor 또는 Old 가 가득 차서 Promotion 실패.
세대 구조 모르면:
세대 구조 알면:
byte[] hugeArray = new byte[100 * 1024 * 1024]; // 100MB
세대 구조 모르면:
세대 구조 알면:
"객체의 일생을 설명해주세요"
"Survivor 가 2개인 이유?"
"Promotion 이 뭔가요?"
답 못함:
잘 답함:
ILIC 운영 환경:
- Heap: 4GB
- 평균 응답: 50ms
- 가끔 응답: 500ms ~ 1s
분석:
GC 로그 → Major GC 가 가끔 발생 (200~800ms)
세대 구조 모르면:
세대 구조 알면:
| 시나리오 | 세대 모르면 | 세대 알면 |
|---|---|---|
| Minor GC 빈도 | 막막함 | Eden 크기 조정 |
| Old 빨리 참 | 원인 모름 | Survivor / Threshold 분석 |
| Promotion Failure | 메시지 못 읽음 | 정확한 진단 |
| 거대 객체 | 처리 모름 | Humongous 인지 |
| 면접 | 탈락 | 시니어 인식 |
| 운영 분석 | 표면적 | 근본 해결 |
→ 세대 구조는 자바 운영의 필수.
┌─────────────────────────────────────────┐
│ Heap │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Young Generation (작음) │ │
│ │ │ │
│ │ ┌────────────┐ │ │
│ │ │ Eden │ (보통 8/10) │ │
│ │ └────────────┘ │ │
│ │ ┌────┐ ┌────┐ │ │
│ │ │ S0 │ │ S1 │ (각 1/10) │ │
│ │ └────┘ └────┘ │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Old Generation (큼) │ │
│ │ (장수 객체) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
기본 비율 (HotSpot JVM):
JVM 옵션으로 조정:
-Xms2g -Xmx2g # 전체 Heap 2GB
-Xmn800m # Young 800MB (또는 -XX:NewSize)
-XX:NewRatio=2 # Young : Old = 1 : 2 (기본)
-XX:SurvivorRatio=8 # Eden : S = 8 : 1 (기본)
객체가 만들어지고 사라지는 전체 흐름:
Customer alice = new Customer("Alice");
// ↑ Eden 에 할당
메모리:
[Eden]
Customer ("Alice") ← 새 객체 위치
[S0] (비어있음)
[S1] (비어있음)
[Old] (다른 객체들)
Eden 이 가득 차면 Minor GC 트리거:
Minor GC 시작:
1. Eden + S0 (또는 S1) 의 살아있는 객체 식별
2. S1 (또는 S0) 으로 복사
3. Eden, 원본 Survivor 비움
예시:
Before Minor GC:
[Eden] Object A (Live), Object B (Garbage), Object C (Live)
[S0] (비어있음)
[S1] (비어있음)
After Minor GC:
[Eden] (비어있음)
[S0] (비어있음)
[S1] Object A (age=1), Object C (age=1) ← 1차 생존
→ Object B 는 Garbage → 사라짐.
다음 Minor GC:
Before:
[Eden] Object D, Object E, Object F (모두 Live)
[S0] (비어있음)
[S1] Object A (age=1), Object C (age=1)
GC 진행:
[Eden + S1] → S0 으로 복사 (방향 바뀜!)
After:
[Eden] (비어있음)
[S0] Object A (age=2), Object C (age=2),
Object D (age=1), Object E (age=1), Object F (age=1)
[S1] (비어있음)
핵심:
객체의 age 가 Tenuring Threshold (기본 15) 도달:
Object A 가 15회 살아남음:
[S0] Object A (age=15) ← Threshold 도달!
↓ Promotion
[Old] Object A
[S0] 비움
Tenuring Threshold:
-XX:MaxTenuringThreshold=15Old 영역이 가득 차면:
Major GC (또는 Full GC):
1. Old 의 살아있는 객체 식별
2. Mark-Sweep-Compact 진행
3. 단편화 해소
효과:
[탄생]
↓
Eden ──Minor GC──→ Survivor (age 1)
↓ Minor GC
Survivor (age 2)
↓ ... (15회)
↓
└──Promotion──→ Old
↓ Major GC
(수거)
대상: Young Generation (Eden + Survivor)
빈도: 자주 (수 초마다)
시간: 빠름 (보통 ~10ms)
STW: 짧음
알고리즘: Copying GC
관찰 가능:
GC(0) Pause Young 25M->2M(64M) 5ms
대상: Old Generation 만
빈도: 가끔 (분~시간 단위)
시간: 느림 (수백 ms ~ 수 초)
STW: 김 (또는 Concurrent)
알고리즘: Mark-Sweep-Compact (또는 Concurrent)
관찰 가능:
GC(50) Pause Old 100M->80M(150M) 500ms
대상: Heap 전체 (Young + Old + Metaspace)
빈도: 매우 가끔 (또는 의도적 트리거)
시간: 매우 느림 (초 단위)
STW: 매우 김
원인:
System.gc() 호출경고: Full GC 가 자주 발생 = 메모리 문제 ⚠️
| Minor GC | Major GC | Full GC | |
|---|---|---|---|
| 대상 | Young | Old | 전체 |
| 빈도 | 자주 | 가끔 | 매우 가끔 |
| 시간 | 빠름 (~ms) | 느림 (~수백 ms) | 매우 느림 (~초) |
| STW | 짧음 | 김 | 매우 김 |
| 알고리즘 | Copying | Mark-Sweep-Compact | 다양 |
| 발생 트리거 | Eden 가득 | Old 가득 | 다양 |
1개라면?:
2개로 운영:
효과:
→ Q1 답: Copying GC 활용 + 단편화 자동 해소.
거대 객체 = Survivor 영역보다 큰 객체.
문제:
해결:
-XX:PretenureSizeThreshold=N[G1 Heap]
├── Eden Region
├── Survivor Region
├── Old Region
└── Humongous Region ← 큰 객체
→ Q2 답: 직접 Old 할당 또는 Humongous 영역.
// 시작 상태:
// Eden: [A][B][C][D][E] (모두 새 객체, B는 Garbage)
// S0: (비어있음)
// S1: (비어있음)
// Old: [oldX][oldY]
Step 1: GC 트리거
Step 2: GC Root 식별
Step 3: 마킹 (Reachability 분석)
Step 4: Copying — 살아있는 객체를 S1 로
After:
Eden: (비어있음)
S0: (비어있음)
S1: [A age=1][C age=1][D age=1][E age=1]
Old: [oldX][oldY]
Step 5: STW 종료
Step 6: 다음 GC
// Object A: age=14
// Tenuring Threshold = 15
// Minor GC 발생:
// A 가 살아남음 → age=15
// → Threshold 도달!
// → Old 로 Promotion
[Survivor]
[A age=15] → 사라짐
[Old]
[oldX][oldY][A] ← 새로 들어옴
Adaptive Tenuring:
# 조정 옵션
-XX:MaxTenuringThreshold=15 # 최대 15회
-XX:TargetSurvivorRatio=50 # Survivor 목표 50%
문제: Old → Young 참조가 있으면 Minor GC 시 Old 도 봐야 함.
예시:
public class FareCache {
private static List<Fare> cache = new ArrayList<>(); // Old (long-lived)
public void add(Fare fare) { // fare 는 Young
cache.add(fare);
// Old 객체 cache 가 Young 객체 fare 참조
}
}
Card Table:
dirty 마킹:
// cache.add(fare) 실행 시 — Write Barrier 발생
// (JVM 이 자동 처리)
// JVM:
// "cache 객체가 fare (Young) 참조 추가"
// → cache 가 있는 카드를 dirty 마킹
Minor GC 시:
1. Stack, static 의 GC Root 스캔
2. dirty 카드만 Old 에서 추가 스캔
3. 모든 Live 객체 마킹
효과:
Write Barrier = 객체 참조 변경 시 JVM 이 자동 실행하는 코드.
역할:
비용:
전형적인 자바 애플리케이션:
Eden: 매우 많은 객체 (수만~수십만)
대부분 곧 Garbage
S0/S1: 적은 객체 (수백~수천)
Eden 의 1~10%
Old: 적당한 객체 (수천~수만)
살아남은 것들
객체 수의 분포:
→ 약한 세대 가설 의 실제 모습.
기존 세대 구조의 진화 — G1 GC 는 Region 기반:
[Heap]
├── Region 0 (Eden)
├── Region 1 (Eden)
├── Region 2 (Survivor)
├── Region 3 (Old)
├── Region 4 (Humongous)
├── Region 5 (Free)
├── Region 6 (Eden)
├── Region 7 (Old)
└── ... (수백 개 Region)
특징:
→ Unit 5.4 에서 자세히.
public class GenerationDemo {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
for (int i = 0; i < 1000; i++) {
// 새 객체 — Eden 에
byte[] data = new byte[1024 * 100]; // 100KB
if (i % 100 == 0) {
System.out.printf("Iteration %d, Free memory: %d MB%n",
i, runtime.freeMemory() / (1024 * 1024));
}
// i % 100 == 0 일 때만 살려둠 (다른 컬렉션에 참조)
if (i % 100 == 0) {
staticList.add(data); // Old 로 가게 만듦
}
}
}
private static List<byte[]> staticList = new ArrayList<>(); // GC Root
}
JVM 옵션 추가:
java -Xmx256m -Xms256m \
-Xlog:gc*:stdout:time,uptime,level,tags \
GenerationDemo
관찰:
java -Xms512m -Xmx512m \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
YourApp
로그 예시:
[0.150s] GC(0) Pause Young (G1 Evacuation Pause)
15M->5M(512M) 8.123ms
[1.234s] GC(1) Pause Young (G1 Evacuation Pause)
15M->6M(512M) 7.456ms
[5.678s] GC(2) Pause Young (G1 Mixed GC) ← Old도 일부 GC
50M->10M(512M) 25.123ms
[60.123s] GC(3) Pause Full (Allocation Failure)
200M->50M(512M) 350.456ms ← Full GC! ⚠️
해석:
@Service
public class FareReportService {
// ❌ 메모리 폭발 가능
public byte[] generateAllFaresReport() {
List<Fare> all = repository.findAll(); // 100만 건?
// → Eden 폭발 → Survivor 거쳐 Old 까지
// → Heap 가득 → OOM 또는 Full GC
return generatePdf(all);
}
// ✅ Streaming
public void generateReportStream(OutputStream out) {
try (Stream<Fare> stream = repository.findAllAsStream()) {
stream.forEach(fare -> {
appendToPdf(out, fare);
// fare 는 사용 후 Eden 의 Garbage 가 됨
// → Minor GC 로 빠르게 회수
});
}
// 메모리 소비 거의 없음
}
}
GC 측면:
@Component
public class FareCache {
private final Map<Long, Fare> cache = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 1000) // 1초마다
public void refresh() {
List<Fare> recent = repository.findRecent();
recent.forEach(f -> cache.put(f.getId(), f));
// ⚠️ Fare 들이 곧바로 Old 로 Promotion
// → cache 가 GC Root 이기 때문
// → Old 영역 빠르게 참
// → Major GC 빈번
}
}
문제:
개선:
@Component
public class FareCache {
private final Cache<Long, Fare> cache =
Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)) // 만료
.maximumSize(1000) // 크기 제한
.build();
// → 적절한 크기 유지
// → Old 영역 안정
}
# JVM 옵션
java -XX:+PrintTenuringDistribution \
-Xlog:gc*:stdout \
YourApp
출력:
Desired survivor size 524288 bytes, new threshold 7 (max 15)
- age 1: 245664 bytes, 245664 total
- age 2: 156784 bytes, 402448 total
- age 3: 89456 bytes, 491904 total
- age 4: 45120 bytes, 537024 total ← 초과!
해석:
→ Old 영역 압박 신호.
// ❌ static 컬렉션 — Old 에 무한 누적
public class FareLogger {
private static List<String> logs = new ArrayList<>();
public static void log(String message) {
logs.add(LocalDateTime.now() + " - " + message);
// logs 가 무한 증가
}
}
GC 관점에서의 문제:
해결:
public class FareLogger {
private static final int MAX_SIZE = 1000;
private static final Deque<String> logs = new ArrayDeque<>(MAX_SIZE);
public static synchronized void log(String message) {
if (logs.size() >= MAX_SIZE) {
logs.pollFirst(); // 오래된 것 제거
}
logs.addLast(LocalDateTime.now() + " - " + message);
}
}
@Service
public class FareCalculator {
// ✅ 짧은 일생의 객체만
public Money calculate(Fare fare, Customer customer) {
// 모두 메서드 안에서만 사용 → Eden 에서 죽음
Money base = Money.of(fare.getAmount());
Money discount = base.multiply(customer.getDiscountRate())
.divide(100);
Money tax = base.subtract(discount).multiply(0.1);
return base.subtract(discount).add(tax);
}
}
GC 패턴:
→ 약한 세대 가설 의 모범 사례.
Q: "객체의 일생?"
A: "Eden → Old 입니다" ❌
정답: Eden → Survivor (여러 회) → Old
Survivor 는 필터링 역할. 진짜 장수 객체만 Old 로.
Q: "Survivor 가 몇 개?"
A: "1개요" ❌
정답: 2개 (S0, S1 또는 From, To)
Copying GC 위해 둘이 번갈아 사용.
Q: "Major GC 와 Full GC 차이?"
A: "같은 거 아닌가요?" ❌
정답:
자주 혼용되지만 엄밀히는 다름.
Q: "객체가 Old 로 가는 조건?"
A: "15회 살아남으면" ❌ (부정확)
정답:
for (int i = 0; i < Integer.MAX_VALUE; i++) {
Fare fare = new Fare();
fares.add(fare); // ⚠️ 누적
}
문제:
해결: 처리 후 참조 끊기, Stream 사용 등.
java -Xmx2g -XX:NewSize=1g # Young 1GB
# 그런데 SurvivorRatio 는 기본 8
# → Eden 800MB, S0/S1 각 100MB
가능한 문제:
튜닝 시 고려:
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1 (기본)
-XX:SurvivorRatio=4 # Eden:S0:S1 = 4:1:1 (Survivor 더 큼)
운영: Full GC 가 매 분마다 발생
개발자: "GC 잘 돌고 있네"
진실: Full GC 는 위험 신호 ⚠️
정상:
매 분 Full GC = 메모리 누수 가능성 높음.
[Unit 5.1: GC 기본 + 약한 세대 가설] ✓
↓
[Unit 5.2: Heap 의 세대 구조] ← 지금 여기 ★
↓
[Unit 5.3: GC 알고리즘 4가지]
↓
[Unit 5.4: GC 종류와 선택 기준]
| 학습 | 세대 구조 관점 |
|---|---|
| Unit 4.1 (Heap) | 세대 구조의 무대 |
| Unit 4.2 (Pass by Value) | 객체 참조 → 세대 추적 |
| Unit 5.1 (약한 세대 가설) | 세대 구조의 이유 |
→ Phase 4-5 가 자연스럽게 연결.
5주차 (Spring DI):
11-12주차 (JPA):
13-14주차 (DB/Cache):
4주차 (동시성):
# Heap 크기
-Xms2g -Xmx2g # 초기/최대 Heap
# Young 크기
-Xmn800m # Young 800MB
-XX:NewRatio=2 # Young : Old = 1 : 2
-XX:NewSize=400m # Young 초기
-XX:MaxNewSize=800m # Young 최대
# Survivor 비율
-XX:SurvivorRatio=8 # Eden : S = 8 : 1 (기본)
# Promotion
-XX:MaxTenuringThreshold=15 # 최대 15회
-XX:TargetSurvivorRatio=50 # Survivor 목표 50%
# 거대 객체
-XX:PretenureSizeThreshold=1m # 1MB 이상은 직접 Old
# 모니터링
-XX:+PrintTenuringDistribution # 세대별 분포 출력
-Xlog:gc* # GC 로그
| 질문 | 이 Unit에서의 답 |
|---|---|
| "객체의 일생?" | Eden → Survivor (15회) → Old |
| "Survivor 가 2개인 이유?" | Copying GC + 단편화 해소 |
| "Minor vs Major vs Full GC?" | 대상 영역과 빈도/시간 차이 |
| "Promotion 이 뭔가요?" | Tenuring Threshold 도달 시 Old 이동 |
| "거대 객체 처리?" | 직접 Old 또는 Humongous (G1) |
| "Card Table?" | Old → Young 참조 추적 |
1️⃣ Heap은 Young (Eden + S0 + S1) 과 Old 로 나뉜다.
약한 세대 가설을 활용해 객체의 수명에 따라 영역 분리. Young 비율 보통 1/3, Eden:S0:S1 = 8:1:1. Eden 에 새 객체 → Minor GC 시 살아남으면 Survivor → 일정 횟수 살아남으면 Old 로 Promotion. 이 구조가 약한 세대 가설의 정확한 구현.
2️⃣ Survivor가 2개인 것은 Copying GC 를 위함이다.
한쪽 (From) 은 사용, 다른 쪽 (To) 은 비어있음. Minor GC 시 [Eden + From] 의 살아있는 객체를 [To] 로 복사 후 원본 비움. 다음에는 역할 바뀜. 자동 압축 효과 + 단편화 해소. 객체의 age 가 MaxTenuringThreshold (기본 15) 도달 시 Old 로 Promotion.
3️⃣ Minor GC vs Major GC vs Full GC 를 정확히 구별해야 한다.
Minor GC = Young 만 (자주, 빠름, ~ms), Major GC = Old 만 (가끔, 느림, ~수백 ms), Full GC = 전체 (매우 가끔, 매우 느림, ~초). Full GC 가 자주 발생 = 메모리 누수 신호 ⚠️. Card Table 로 Old → Young 참조를 추적해 Minor GC 효율 유지. 거대 객체는 직접 Old 할당 또는 Humongous (G1).
Q1: Survivor 가 두 개 (From, To) 로 나뉘어 있는 이유는?
한 줄 답: Copying GC 를 활용하기 위해. 한쪽은 항상 비어있어 살아있는 객체를 그쪽으로 복사하면 자동 압축 + 단편화 해소 효과.
상세 설명:
만약 Survivor 가 1개라면:
[Eden] [Survivor]
새 객체 살아남은 객체
Minor GC:
- Eden 의 살아있는 객체를 Survivor 로 복사?
- 그런데 Survivor 안에는 이미 객체들 있음
- → 충돌! 또는 복잡한 압축 로직 필요
문제:
[Eden] [Survivor 0 (From)] [Survivor 1 (To)]
새 객체 현재 거주 비어있음
Minor GC 시:
1. [Eden + Survivor 0] 의 살아있는 객체 식별
2. 모두 [Survivor 1] 로 복사
3. [Eden] 비움
4. [Survivor 0] 비움
After:
[Eden] [Survivor 0] [Survivor 1]
비움 비움 현재 거주
다음 Minor GC:
1. [Eden + Survivor 1] → [Survivor 0]
2. 역할 바뀜!
핵심 효과 ⭐ :
손님 (객체) 이 호텔에 머무는 상황:
1개 방만 있다면:
2개 방 있다면 (자바 방식):
→ 간단하고 효율적.
"Survivor 가 2개인 것은 Copying GC 의 자연스러운 요구.
한 영역에서 다른 영역으로의 단순 복사가 가장 효율적이고,
자동 압축 효과 + 단편화 해소 라는 부수 효과까지."
Q2: 거대 객체 (Survivor 영역보다 큰) 는 어떻게 처리되는가?
한 줄 답: 직접 Old 영역에 할당 또는 Humongous Region (G1 GC).
상세 설명:
byte[] hugeArray = new byte[100 * 1024 * 1024]; // 100MB
거대 객체 = Survivor 영역보다 큰 객체.
왜 문제인가?:
JVM 옵션:
-XX:PretenureSizeThreshold=1m # 1MB 이상은 직접 Old
동작:
byte[] huge = new byte[10 * 1024 * 1024]; // 10MB
// Eden 거치지 않고 → Old 직접 할당
효과:
단점:
G1 GC 는 Region 기반:
[Heap]
├── Region 0 (Eden, 1MB)
├── Region 1 (Eden, 1MB)
├── Region 2 (Survivor, 1MB)
├── Region 3 (Old, 1MB)
├── Region 4 (Humongous, 1MB) ← 큰 객체
└── ... 수백 개
Humongous Object 정의:
예시:
Region 크기 1MB
객체 크기 3MB
→ Humongous Region 으로 3개 연속 사용
JVM 옵션:
-XX:G1HeapRegionSize=4m # Region 4MB
효과:
상황: 운임 보고서 PDF 생성
@Service
public class FareReportService {
public byte[] generateMonthlyReport() {
// 100MB PDF 생성?
byte[] pdfData = generatePdf(...); // 거대 객체
return pdfData;
}
}
문제:
해결:
1. Streaming:
public void streamReport(OutputStream out) {
generatePdfStream(out); // 메모리에 다 안 올림
}
Pretenuring:
G1 + Humongous:
"거대 객체는 일반 객체와 다른 경로로 처리.
Pretenuring (직접 Old) 또는 G1의 Humongous Region.
가능하면 Streaming 등으로 거대 객체 자체를 만들지 않는 것이 최선."















