F-lab Java 1주차 / Phase 5 / Unit 5.1 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 4.1 (JVM 런타임 데이터 영역)
다음 Unit: 5.2 — Heap의 세대 구조이 Unit의 의미: Phase 5 의 시작 — GC 의 출발점.
면접에서 거의 100% 출제 되는 GC 영역의 기본기.
운영 환경에서 OOM, 성능 저하 분석의 토대.
당신이 큰 사무실에서 일한다고 상상해보세요.
상황:
해결 — 청소부 (GC):
청소부의 일하는 방식:
1. 둘러보기 — "이 컵 누가 쓰나?"
2. 사용 중이면 → 그대로 둠
3. 사용 안 하면 → 버림
4. 정리 후 공간 확보
→ 이게 GC 의 본질.
만약 청소부가 없다면 (C/C++)?
free, delete)도서관에 매일 새 책이 들어옵니다:
도서관 사서의 정리 전략:
1. 신간 코너 (Eden) — 새 책은 일단 여기
2. 인기 코너 (Survivor) — 가끔 빌리는 책
3. 일반 서가 (Old) — 계속 빌리는 책
4. 폐기 — 아무도 안 빌리는 책
핵심 통찰:
→ 이게 약한 세대 가설(Weak Generational Hypothesis). GC 의 핵심 통찰.
"GC 는 더 이상 참조되지 않는 객체를 자동으로 수거하는 메커니즘이다."
GC의 두 가지 핵심 개념:
비유 정리:
| 비유 요소 | GC 적용 |
|---|---|
| 청소부 | GC |
| 사무실 | Heap |
| 사용 안 하는 컵 | Garbage (참조 없는 객체) |
| 사용 중인 책상 | Live 객체 (참조 있음) |
| 청소 시간 | GC 실행 시간 |
개발자가 직접:
// C 코드
int* arr = malloc(sizeof(int) * 100); // 할당
// ... 사용 ...
free(arr); // 직접 해제
장점:
단점:
언어가 자동으로:
// Java 코드
int[] arr = new int[100]; // 할당
// ... 사용 ...
// 해제? 자동으로 GC 가 처리 ✅
장점:
단점:
자바 (1995) 의 핵심 약속 중 하나:
"메모리 관리는 JVM 이 알아서"
이를 위해 Garbage Collector 도입.
자바 GC 의 영향:
자바 GC 는 계속 진화해왔습니다:
[Java 1.0~1.4]
- 단순 Mark-Sweep
- Stop-the-World
- 매우 느림 (전체 멈춤)
[Java 5]
- Generational GC 도입 (세대별 GC)
- Young/Old 분리
[Java 6, 7]
- Parallel GC (멀티스레드)
- CMS (Concurrent Mark Sweep)
- 더 짧은 STW
[Java 9]
- G1 GC 가 기본
- 큰 Heap 에 적합
[Java 11+]
- ZGC (저지연)
- Shenandoah (저지연)
- 수십 GB Heap 에서도 ms 단위 STW
[Java 21+]
- Generational ZGC
- 더 효율적
→ GC 진화 = 자바 진화.
기본 질문:
"Heap 의 어떤 객체가 사용 중 이고, 어떤 게 Garbage 인가?"
판단 기준:
public void method() {
String s = "hello"; // s가 String 참조 (Live)
}
// 메서드 종료 → s 사라짐 → "hello" 객체 참조 X → Garbage
참조의 출발점 (GC Root) ⭐ :
GC 가 효율적으로 동작하기 위한 가장 중요한 가정:
"대부분의 객체는 금방 죽고, 오래된 객체가 젊은 객체를 참조하는 경우는 드물다."
관찰:
1. 대부분의 객체는 곧 Garbage (지역변수, 임시 객체 등)
2. 일부만 오래 살아남음 (캐시, 싱글톤 등)
3. 오래된 객체 → 젊은 객체 참조는 드뭄
실측 통계:
의의:
→ 세대별 GC (Generational GC) 의 토대.
"GC 는 단순한 청소가 아니라 '약한 세대 가설' 이라는 통찰에서 출발한 정교한 시스템이다."
모든 객체를 같게 다루면 비효율적. 객체의 일생 패턴 (대부분 단명, 일부 장수) 을 활용해 영역을 나누고 다른 전략으로 청소. 이게 자바 GC 의 핵심 통찰.
이 통찰이 없으면 자바 백엔드는 운영 환경에서 너무 자주 멈춤. 통찰이 있어서 수 GB Heap 에서도 ms 단위 STW 가 가능.
GC 이해 없이 자바를 운영하면 다양한 문제에 부딪힙니다.
// 운영 환경
// 새벽 3시, Slack 알림 폭주
// "API 응답 안 됨"
// 로그 확인:
java.lang.OutOfMemoryError: Java heap space
at java.util.ArrayList.grow(ArrayList.java:...)
...
GC 모르면:
-Xmx16gGC 알면:
정상: 평균 응답 시간 50ms
이상: 가끔 500ms ~ 2초
GC 모르면:
GC 알면:
-Xlog:gc*)@Service
public class FareReportService {
public byte[] generateMonthlyReport() {
List<Fare> allFares = fareRepository.findAll(); // ⚠️
// 만약 100만 건이면?
// → Heap 메모리 부족
// → OOM
}
}
GC 모르면:
GC 알면:
MemoryConsumptionMonitor 도입public class FareCache {
private static final Map<Long, Fare> cache = new HashMap<>();
public Fare get(Long id) {
return cache.computeIfAbsent(id, this::loadFromDB);
}
}
// → cache 가 영원히 증가, 절대 GC 안 됨
// → 시간이 지날수록 OOM 가까워짐
GC 모르면:
GC 알면:
면접 단골:
"GC 가 뭔가요?"
"Stop-the-World 가 뭐죠?"
"약한 세대 가설을 설명해주세요"
답 못함:
잘 답함:
운영 사고 후:
"Heap Dump 떴습니다. 분석 부탁드려요"
GC 모르면:
GC 알면:
| 시나리오 | GC 모르면 | GC 알면 |
|---|---|---|
| OOM | 메모리 늘리기 | 누수 코드 찾기 |
| 지연 | 원인 못 찾음 | GC 튜닝 |
| 메모리 누수 | 모니터링만 | 코드 수정 |
| 면접 | 탈락 | 시니어 인식 |
| Heap Dump | 못 읽음 | 정확히 분석 |
→ GC 는 시니어 자바 개발자의 필수 영역.
Garbage = "더 이상 참조되지 않는 객체"
public void method() {
Customer c = new Customer("Alice"); // 객체 1: 참조 있음 (Live)
// ...
c = new Customer("Bob"); // c 가 객체 2 가리킴
// 객체 1 → 참조 없음 → Garbage
}
// 메서드 종료 → c 사라짐 → 객체 2도 Garbage
메모리 그림:
[Heap]
Customer ("Alice") ← Garbage (아무도 참조 X)
Customer ("Bob") ← 메서드 종료 후 Garbage
[Stack]
c → 메서드 종료 시 사라짐
GC Root: 참조 추적의 시작점
GC Root 의 종류:
1. Stack 의 지역변수 (모든 활성 스레드)
2. Method Area 의 static 변수
3. JNI 참조 (native 코드)
4. Synchronized lock 객체
Live 판단:
"GC Root 에서 도달 가능한 객체 = Live"
"도달 불가능 = Garbage"
예시:
public class Main {
static List<Customer> customers = new ArrayList<>(); // GC Root (static)
public static void main(String[] args) { // args, customers 는 Live
Customer alice = new Customer("Alice"); // alice 는 Stack → GC Root
customers.add(alice); // alice 도 customers 에서 도달 가능
Customer bob = new Customer("Bob"); // bob 은 Stack → GC Root
// bob 은 customers 에 추가 X
// ...
}
}
도달 가능성 그래프:
[GC Root]
- args
- alice ─────┐
- bob ───────┤
- customers ─┤ (static)
↓
[Heap]
Customer ("Alice") ← alice 와 customers 양쪽에서 도달 (Live)
Customer ("Bob") ← bob 만 도달 (Live)
→ 메서드 종료 시:
1. GC Root 식별
↓
2. Root 에서 도달 가능한 객체 마킹 (Live)
↓
3. 마킹 안 된 객체 = Garbage
↓
4. Garbage 메모리 해제
↓
5. (선택) 살아있는 객체 압축 (Compact)
→ 5주차 GC 알고리즘 (5.3) 에서 자세히.
STW = "GC 실행 동안 모든 애플리케이션 스레드 정지"
왜 필요?:
STW 의 영향:
예시:
정상 응답: ┃━━━━━━━━━━━━━━━━━━━━┃ (50ms)
STW 발생: ┃━━━━┃[GC: 200ms]┃━━━━━┃ (250ms)
→ STW 를 줄이는 게 GC 진화의 방향.
STW 가 긴 GC:
STW 가 짧은 GC:
"대부분의 객체는 금방 죽고 (단명),
살아남은 객체는 오래 산다 (장수)."
경험적 통찰:
일반적 객체 패턴:
public void process() {
String temp = "임시 데이터"; // 단명 — 메서드 종료 시 Garbage
List<Integer> list = new ArrayList<>(); // 단명
// ...
}
@Service
public class FareService { // 장수 — 애플리케이션 평생
private FareRepository repository;
}
가설의 활용 ⭐ :
만약 객체를 세대 (Generation) 로 나누면?
효과:
→ 이 통찰이 자바 GC 의 핵심.
1. 새 객체 → Young Generation 의 Eden
↓
2. Eden 가득 참 → Minor GC
↓
3. 살아남은 객체 → Survivor 영역
↓
4. 일정 횟수 살아남으면 → Old Generation Promotion
↓
5. Old 가득 참 → Major GC (또는 Full GC)
→ Unit 5.2 에서 본격.
GC 에 영향을 미치는 4가지 참조:
Customer c = new Customer(); // 강한 참조 — 절대 GC X
SoftReference<Customer> sr = new SoftReference<>(new Customer());
// 메모리 부족 시 GC
WeakReference<Customer> wr = new WeakReference<>(new Customer());
// 다음 GC 시 수거
PhantomReference<Customer> pr = new PhantomReference<>(...);
// GC 발생 후 알림용
→ Phase 6 에서 자세히.
GC 의 가장 단순한 알고리즘:
[Heap]
Object1 ───→ Object2
↑
│
[Stack]
ref1 ─────────┘
GC: "ref1 에서 시작..."
GC: "Object2 도달 → 마킹"
GC: "Object1 도달 → 마킹"
[Heap]
Object1 (마킹 ✓) — 보존
Object2 (마킹 ✓) — 보존
Object3 (마킹 ✗) — 제거
Object4 (마킹 ✗) — 제거
결과:
[Heap]
Object1
Object2
(Object3, 4 자리 비어있음)
→ 단편화 (Fragmentation) 발생.
Mark-Sweep 후 압축 단계 추가:
Sweep 후:
[Heap]
[Object1] [____] [Object2] [____] [Object3]
(빈 공간)
Compact 후:
[Heap]
[Object1] [Object2] [Object3] [_______________]
(모든 빈 공간이 뒤에)
효과:
비용:
아이디어: 약한 세대 가설을 활용해 영역 분리.
[Heap]
├── Young Generation (작음, 자주 GC)
│ ├── Eden: 새 객체
│ ├── Survivor 0 (From)
│ └── Survivor 1 (To)
│
└── Old Generation (큼, 가끔 GC)
└── 장수 객체들
GC 종류:
→ Unit 5.2 에서 자세히.
문제: Young 객체를 GC 할 때 Old 객체가 참조하면?
[Old Generation]
oldObj → ...
↓ 참조
[Young Generation]
youngObj
만약 youngObj 가 oldObj 에서만 참조되면 Live 인데, Young 만 스캔하면 못 찾음.
해결 — Card Table ⭐ :
[Card Table]
Card 0: clean
Card 1: dirty ← Young 참조 있음
Card 2: clean
Card 3: dirty ← Young 참조 있음
→ 약한 세대 가설 덕분에 dirty 카드 적음 → 효율적.
Safe Point:
STW 흐름:
1. GC 트리거 (Heap 가득 등)
↓
2. 모든 Application Thread 에 "Safe Point 로!" 신호
↓
3. 모든 스레드가 Safe Point 도달
↓
4. STW 시작 (모든 Application Thread 정지)
↓
5. GC 수행 (Mark, Sweep, Compact)
↓
6. STW 종료
↓
7. Application Thread 재개
STW 의 길이 = GC 의 핵심 성능 지표:
1GB Heap, 512 byte 카드 → 카드 수 ≈ 200만 개
각 카드 1 byte → Card Table ≈ 2MB
→ Heap 의 0.2% 정도 메모리 비용 — 감내 가능.
new Object(); // Eden 가득 차서 할당 실패
// → Minor GC 트리거
Old Generation 75% 이상 → Major GC 트리거 (G1 등)
System.gc(); // GC 요청 — JVM 이 무시할 수도 있음
⚠️ System.gc() 직접 호출은 비권장 (성능 예측 불가).
Marking:
GC Root → 도달 객체 마킹
Reference 처리:
WeakReference: GC 대상 (다음 사이클)
SoftReference: 메모리 부족 시
Finalization:
finalize() 메서드 호출 (deprecated)
Sweep:
마킹 안 된 객체 메모리 해제
Compact (선택):
살아있는 객체 압축
→ finalize() 는 Java 9+ 에서 deprecated. AutoCloseable 사용 권장.
public class GCDemo {
public static void main(String[] args) {
System.out.println("시작 메모리: " + getUsedMemory() + " MB");
// 1MB 객체 100개 생성
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(new byte[1024 * 1024]);
}
System.out.println("100MB 할당 후: " + getUsedMemory() + " MB");
list = null; // 참조 끊음
// GC 요청 (강제는 아님)
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("GC 후: " + getUsedMemory() + " MB");
}
private static long getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
}
}
출력 예시:
시작 메모리: 5 MB
100MB 할당 후: 105 MB
GC 후: 5 MB ← GC 가 100MB 회수
→ GC 가 정말 동작함을 확인.
# Java 9+
java -Xlog:gc* -jar myapp.jar
# 또는 자세히
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar myapp.jar
로그 예시:
[0.123s][info][gc] GC(0) Pause Young (G1 Evacuation Pause)
25M->5M(64M) 12.345ms
해석:
0.123s — JVM 시작 후 시간GC(0) — 0번째 GCPause Young — Minor GCG1 Evacuation Pause — G1 GC 의 종류25M->5M(64M) — GC 전 25MB, 후 5MB, 전체 Heap 64MB12.345ms — 소요 시간// ❌ 메모리 누수 — static 컬렉션
public class FareCache {
private static final Map<Long, Fare> cache = new HashMap<>();
public static void cache(Fare fare) {
cache.put(fare.getId(), fare);
// 한번 들어간 fare 는 절대 GC 안 됨
// → 시간이 지날수록 메모리 누수
}
}
왜 누수?:
cache 는 static → GC Root해결 1: 만료 시간:
public class FareCache {
private static final Cache<Long, Fare> cache =
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
}
해결 2: WeakHashMap:
private static final Map<Long, Fare> cache = new WeakHashMap<>();
// 키가 GC 되면 entry 도 자동 제거
// ❌ 누수 가능
public class FareEventBus {
private final List<FareListener> listeners = new ArrayList<>();
public void register(FareListener listener) {
listeners.add(listener);
}
// unregister 메서드 없음 → 등록된 listener 영원히 참조
}
@Component
public class TempFareListener implements FareListener {
@PostConstruct
public void init() {
eventBus.register(this); // 등록만 하고 해제 안 함
}
}
해결:
public class FareEventBus {
private final List<FareListener> listeners = new ArrayList<>();
public void register(FareListener listener) {
listeners.add(listener);
}
public void unregister(FareListener listener) {
listeners.remove(listener);
}
}
@Component
public class TempFareListener implements FareListener {
@PreDestroy
public void cleanup() {
eventBus.unregister(this); // ← 해제
}
}
// ❌ 누수 — Inner Class 의 외부 참조
public class FareService {
private List<byte[]> hugeData = new ArrayList<>(); // 큰 데이터
public Runnable createTask() {
return new Runnable() { // Inner Class
@Override
public void run() {
System.out.println("작업 실행");
// hugeData 사용 안 하지만 외부 참조 자동 보유
}
};
}
}
Runnable task = service.createTask();
service = null; // FareService 참조 끊음 시도
// 그러나 task 안의 Inner Class 가 FareService 참조 보유
// → service 객체 GC 안 됨
// → hugeData 도 함께 메모리 점유
해결: Static Nested Class 또는 람다:
public Runnable createTask() {
// 람다 — hugeData 참조 안 하면 외부 참조 X
return () -> System.out.println("작업 실행");
}
→ Unit 2.6 의 학습이 GC 에 직결.
// 큰 데이터 처리 — Stream 활용
@Service
public class FareReportService {
// ❌ OOM 위험
public byte[] generateReport() {
List<Fare> all = fareRepository.findAll(); // 100만 건이면?
return generatePdfFromAll(all);
}
// ✅ Streaming
public void streamReport(OutputStream out) {
try (Stream<Fare> stream = fareRepository.findAllAsStream()) {
stream.forEach(fare -> appendToPdf(out, fare));
}
// 한 번에 메모리에 다 안 올림
}
}
Spring Data JPA 의 Stream:
@Repository
public interface FareRepository extends JpaRepository<Fare, Long> {
@Query("SELECT f FROM Fare f")
Stream<Fare> findAllAsStream();
}
→ GC 친화적 코드 = 메모리 효율 코드.
# 1. JVM 옵션으로 OOM 시 자동 덤프
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/heap-dump.hprof \
-jar myapp.jar
# 2. 명시적 덤프
jcmd <pid> GC.heap_dump /tmp/heap-dump.hprof
# 3. jmap (Java 8 이전)
jmap -dump:format=b,file=/tmp/heap-dump.hprof <pid>
분석 도구:
→ ILIC 운영에서 OOM 사고 시 즉시 덤프 분석.
public void process() {
// 처리 ...
System.gc(); // ❌ 권장 X
}
왜?:
원칙: GC 는 JVM 에 맡김. 명시적 호출 X.
"자바는 GC 가 있으니 메모리 누수 X 다"
진실: GC 는 참조 끊긴 객체 만 수거.
누수 가능 케이스:
→ GC 가 자동이지만 코드 설계는 책임.
public class FileHandler {
@Override
protected void finalize() throws Throwable { // ❌ deprecated
// 정리 작업
}
}
왜?:
해결: AutoCloseable + try-with-resources:
public class FileHandler implements AutoCloseable {
@Override
public void close() {
// 정리 작업
}
}
try (FileHandler handler = new FileHandler()) {
// 사용
} // 자동 close
// ❌ OOM 위험
List<Fare> all = repository.findAll(); // 1000만 건?
해결: Pagination 또는 Streaming:
// Pagination
Page<Fare> page = repository.findAll(PageRequest.of(0, 1000));
// Stream
try (Stream<Fare> stream = repository.findAllAsStream()) {
stream.forEach(this::process);
}
java -Xmx32g -jar myapp.jar # ❌ 근본 해결 X
문제:
해결: 코드 레벨 메모리 분석 + GC 튜닝.
java -jar myapp.jar # 기본 GC 사용
Java 8 의 기본: Parallel GC
Java 9+ 의 기본: G1 GC
상황에 따라: ZGC, Shenandoah 등
원칙:
운영 환경:
해결: 항상 GC 로그 활성화:
java -Xlog:gc*:file=gc.log:time,uptime,level,tags \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/ \
-jar myapp.jar
[Unit 5.1: GC 기본 개념과 약한 세대 가설] ← 지금 여기
↓
[Unit 5.2: Heap 의 세대 구조]
↓
[Unit 5.3: GC 알고리즘 종류] (있다면)
| 학습 | GC 관점 |
|---|---|
| Unit 4.1 (Heap) | GC 의 주 대상 |
| Unit 4.1 (Stack) | GC Root 의 출발점 |
| Unit 4.2 (Pass by Value) | 객체 참조의 동작 |
| Unit 2.6 (Inner Class) | 외부 참조 보유 → GC 영향 |
→ Phase 4 가 GC 이해의 토대.
4주차 (동시성):
5주차 (Spring):
11-12주차 (JPA):
13-14주차 (DB/Cache):
18주차 (Spring Security):
| 언어 | GC 방식 |
|---|---|
| Java | Generational, Mark-Sweep-Compact |
| C#/.NET | Generational, 비슷 |
| Go | Concurrent Mark-Sweep |
| Python | Reference Counting + Cycle Detection |
| JavaScript (V8) | Generational |
| Rust | GC 없음 (소유권 시스템) |
→ 자바 GC 는 가장 정교한 GC 중 하나.
| 질문 | 이 Unit에서의 답 |
|---|---|
| "GC 가 뭔가요?" | 참조 끊긴 객체 자동 수거 |
| "Garbage 의 정의?" | 어디서도 도달 불가능한 객체 |
| "약한 세대 가설?" | 대부분 객체는 단명, 살아남은 것은 장수 |
| "STW 가 뭔가요?" | GC 동안 모든 스레드 정지 |
| "Mark-Sweep?" | 마킹 + 수거 알고리즘 |
| "GC Root?" | Stack 지역변수, static, JNI 등 |
1️⃣ GC 는 더 이상 참조되지 않는 객체를 자동 수거하는 메커니즘이다.
Garbage = 어디서도 도달 불가능한 객체. GC Root (Stack 지역변수, Method Area static, JNI 참조) 에서 출발해 Reachability 분석으로 Live 객체 마킹, 나머지를 수거. C/C++ 의 수동 메모리 관리의 위험 (메모리 누수, 댕글링 포인터) 을 해결한 자바의 핵심 약속.
2️⃣ 약한 세대 가설이 자바 GC 효율의 비밀이다.
"대부분 객체는 단명, 살아남은 것은 장수" — 이 통찰로 Heap 을 Young Generation (자주 GC) 과 Old Generation (가끔 GC) 으로 분리. Young 의 Eden, Survivor 영역에서 객체가 일정 횟수 살아남으면 Old 로 Promotion. Card Table 로 Old → Young 참조를 추적해 Minor GC 효율 유지.
3️⃣ STW 와 GC 알고리즘 이해가 시니어의 차별화 영역이다.
Stop-the-World = GC 동안 모든 Application Thread 정지. STW 의 길이가 응답 시간에 직결. Java 진화: Serial → Parallel → CMS → G1 → ZGC → Shenandoah, STW 를 줄이는 방향. 운영 환경에서 GC 로그 (
-Xlog:gc*) + Heap Dump (-XX:+HeapDumpOnOutOfMemoryError) + MAT 분석은 시니어의 필수 도구.System.gc()호출,finalize()사용은 안티패턴.
Q1: 약한 세대 가설이 맞다면 메모리 구조를 어떻게 설계하는 게 합리적인가?
한 줄 답: 세대별로 영역을 분리 (Young / Old) 해서 각자 다른 GC 전략 적용.
상세 설계 — Generational Heap:
[Heap]
├── Young Generation (작음)
│ ├── Eden (새 객체)
│ ├── Survivor 0 (1차 생존)
│ └── Survivor 1 (2차 생존)
│
└── Old Generation (큼)
└── 장수 객체
왜 이렇게?:
Young GC 알고리즘 — Copying GC (Eden + Survivor):
1. 새 객체 → Eden 에 할당
2. Eden 가득 → Minor GC
3. Eden + Survivor 0 의 살아있는 객체 → Survivor 1 로 복사
4. Eden, Survivor 0 비움
5. 다음 GC 시 역방향 (Survivor 1 → Survivor 0)
6. N회 살아남으면 → Old 로 Promotion
효율적인 이유:
Old GC 알고리즘 — Mark-Sweep-Compact:
Card Table 도입:
→ 이 모든 것이 약한 세대 가설 의 자연스러운 결과.
자바의 실제 = HotSpot JVM 이 정확히 이 구조 채택.
Q2: "GC 튜닝" 이 일반적으로 무엇을 줄이는 것인가?
한 줄 답: STW (Stop-the-World) 시간 을 줄이는 것.
상세 설명:
GC 튜닝의 주요 목표는 3가지 지만, 가장 흔한 것은 STW:
왜 중요?:
측정:
튜닝 방법:
예시:
# Parallel GC (Java 8 기본): STW 길음
java -XX:+UseParallelGC -Xmx8g
# G1 GC (Java 9+ 기본): STW 짧음
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx8g
# ZGC (Java 11+): STW 매우 짧음 (~ms)
java -XX:+UseZGC -Xmx8g
Throughput = 애플리케이션 작업 시간 / 전체 시간
왜 중요?:
튜닝 방법:
# Parallel GC + Throughput 우선
java -XX:+UseParallelGC -XX:GCTimeRatio=99 -Xmx16g
왜 중요?:
튜닝 방법:
[STW 짧음] ←————————→ [Throughput 높음]
ZGC Parallel
저지연 배치/처리량
모두 만족 불가 — 상황에 맞는 선택.
Step 1: GC 로그 켜기
-Xlog:gc*:file=gc.log
Step 2: 측정
- 평균 GC 시간
- Full GC 빈도
- Heap 사용 패턴
Step 3: 분석
- GCViewer, gceasy.io 등 도구
Step 4: 가설 + 변경
- 알고리즘 변경
- Heap 크기 조정
- Young/Old 비율
Step 5: 재측정
- 효과 검증
ILIC 환경에서의 일반 권장:
# 일반적 백엔드 (Spring Boot)
java -XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-Xms2g -Xmx2g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/ \
-Xlog:gc*:file=/var/log/gc.log \
-jar ilic.jar
핵심 옵션:
UseG1GC: G1 GC (Java 9+ 기본)MaxGCPauseMillis=200: 목표 STW 200msXms == Xmx: Heap 고정 (재할당 비용 회피)HeapDumpOnOutOfMemoryError: OOM 시 덤프