GC 튜닝 기초 (왜 대부분의 성능 문제는 GC보다 객체 생성 패턴에서 시작되는가)
Java 개발을 하다 보면 이런 말을 자주 듣는다.
GC 때문에 서버가 느려졌어요.
GC 튜닝하면 빨라질까요?
-Xmx를 늘리면 해결되나요?
하지만 실무에서는 GC 자체가 문제인 경우보다
애플리케이션이 GC를 힘들게 만드는 경우가 훨씬 많다.
GC는 메모리를 회수하는 도구일 뿐이다.
메모리를 과도하게 사용하는 코드, 불필요한 객체를 계속 생성하는 코드, 캐시를 잘못 사용하는 코드가 있다면 아무리 좋은 GC를 사용해도 근본적인 해결이 되지 않는다.
이번 글에서는
- JVM 메모리 구조와 GC의 관계
- Minor GC / Major GC
- Stop-The-World
- GC 튜닝의 기본 원칙
- 실무에서 GC를 바라보는 방법
까지 알아본다.
왜 GC가 필요한가?
C/C++에서는
User* user = new User();
delete user;
처럼 개발자가 직접 메모리를 해제한다.
만약
delete
를 하지 않으면
메모리 누수가 발생한다.
Java는 다르다.
User user = new User();
사용이 끝난 후
user = null;
또는
user = new User();
처럼 기존 객체를 더 이상 참조하지 않으면
GC가 자동으로 메모리를 회수한다.
GC는 언제 동작할까?
많은 사람들이
객체가 없어지면 바로 GC
라고 생각한다.
그렇지 않다.
GC는 JVM이 필요하다고 판단할 때 실행된다.
예를 들어
User user = new User();
user = null;
이 순간
객체는
GC 대상
이 될 뿐이다.
즉시 삭제되지 않는다.
Heap 구조 다시 보기
GC는 Heap에서만 동작한다.
Heap은 크게 두 영역으로 나뉜다.
Heap
├── Young Generation
│ ├── Eden
│ ├── Survivor 0
│ └── Survivor 1
│
└── Old Generation
객체는 대부분
Eden
에서 생성된다.
객체 생성 과정
예를 들어
new User();
실행
↓
Eden
생성
↓
사용 종료
↓
GC 대상
↓
삭제
대부분의 객체는
Old Generation까지 가지도 못한다.
Weak Generational Hypothesis
현대 GC의 핵심 가설이다.
내용은 단 하나다.
대부분의 객체는 금방 죽는다.
예를 들어
String name = request.getName();
UserDto dto = new UserDto();
List<User> list = repository.findAll();
이런 객체들은
메서드가 끝나면 대부분 사라진다.
그래서
Young Generation을 자주 청소하는 것이 효율적이다.
Minor GC
Young Generation만 청소하는 GC
Eden 가득 참
↓
Minor GC
↓
살아있는 객체만 Survivor 이동
↓
나머지 삭제
예시
Eden
100개 객체
↓
90개 죽음
↓
10개만 Survivor 이동
매우 효율적이다.
Survivor 영역
Young Generation에는
Survivor 0
Survivor 1
이 존재한다.
객체는
Eden
↓
S0
↓
S1
↓
S0
↓
S1
처럼 번갈아 이동한다.
왜?
메모리 복사가 매우 빠르기 때문이다.
Promotion
Minor GC를 여러 번 살아남으면
Old Generation으로 이동한다.
이를
Promotion
이라고 한다.
예시
Young
↓
Minor GC
↓
Minor GC
↓
Minor GC
↓
Old Generation
대표적인 객체
- Spring Singleton Bean
- Cache
- Session
- static 객체
Old Generation
여기는
오랫동안 살아있는 객체가 존재한다.
예를 들어
private final Map cache = new HashMap();
이런 객체는
애플리케이션 종료 전까지 살아있을 수도 있다.
Old Generation은
Young보다 훨씬 크다.
GC도 훨씬 오래 걸린다.
Major GC / Full GC
Old Generation을 청소하는 GC
Old Generation
↓
Mark
↓
Sweep
↓
Compact
이 과정을 수행한다.
Young보다 훨씬 느리다.
Stop-The-World(STW)
GC에서 가장 중요한 개념
GC가 실행되는 동안
애플리케이션 스레드 대부분이 일시 정지된다.
이를
Stop-The-World
라고 한다.
그림으로 보면
사용자 요청
↓
API 실행
↓
GC 발생
↓
모든 작업 일시 정지
↓
GC 종료
↓
다시 실행
즉
GC 시간은
사용자 입장에서
응답 지연 시간이다.
STW가 항상 나쁜 것은 아니다
많은 사람들이
STW = 무조건 나쁨
이라고 생각한다.
하지만
Minor GC는
보통 수 ms 이하로 끝나는 경우가 많다.
예를 들어
Minor GC
2ms
정도라면
대부분의 서비스는 체감하지 못한다.
문제는
Full GC
1초
2초
5초
처럼 길어질 때다.
GC 튜닝보다 중요한 것
실무에서
GC 로그를 보면
많은 경우 원인은
GC가 아니다.
예를 들어
while(true){
new User();
}
초당 수천만 객체 생성
↓
Minor GC 폭증
↓
GC 문제처럼 보임
실제로는
객체 생성 패턴이 문제다.
흔한 GC 유발 코드
String 연결
String result = "";
for(...) {
result += value;
}
중간 String 객체 폭증
큰 컬렉션
List<User> users =
repository.findAll();
100만 건 조회
↓
100만 객체 생성
무한 캐시
Map cache = new HashMap();
계속 put()
삭제 안 함
↓
Old Generation 증가
불필요한 Boxing
Long total = 0L;
Wrapper 객체 생성 증가
GC 튜닝은 언제 할까?
실무 순서
① GC 로그 확인
↓
② Heap 사용량 확인
↓
③ 객체 생성 패턴 확인
↓
④ 메모리 누수 여부 확인
↓
⑤ GC 옵션 변경
즉
GC 옵션은
마지막 단계다.
대표적인 JVM 옵션
-Xms2g
-Xmx2g
옵션 설명
- -Xms2g
- JVM 시작 시 Heap 초기 크기를 2GB로 설정한다.
- -Xmx2g
- Heap 최대 크기를 2GB로 제한한다.
-XX:+UseG1GC
옵션 설명
- -XX:+UseG1GC
- G1 GC를 사용하도록 지정한다.
-Xlog:gc*
(JDK 9 이상)
옵션 설명
- -Xlog:gc*
- GC 관련 로그를 출력한다.
JDK 8에서는
-XX:+PrintGCDetails
옵션 설명
- -XX:+PrintGCDetails
- GC 상세 로그를 출력한다.
실무에서 GC 로그로 가장 먼저 보는 것
GC 로그를 분석할 때 가장 먼저 확인하는 항목은 다음과 같다.
- Minor GC가 너무 자주 발생하는가?
- Full GC가 발생하는가?
- GC 한 번에 얼마나 멈추는가?
- Heap 사용량이 계속 증가하는가?
- Old Generation이 계속 차오르는가?
이 다섯 가지를 먼저 확인하면 대부분의 문제 원인을 좁힐 수 있다.
지금까지의 연결
이번 시리즈에서 이어지는 흐름은 다음과 같다.
객체 생성
↓
TLAB
↓
Escape Analysis
↓
Young Generation 생성
↓
Minor GC
↓
Promotion
↓
Old Generation
↓
Major GC
즉
객체 생성부터 GC까지는 하나의 흐름으로 이어진다.
실무에서 가장 중요한 원칙
GC를 빠르게 만드는 것보다
GC가 해야 할 일을 줄이는 것이 훨씬 중요하다.
이를 위해
- 불필요한 객체 생성을 줄이고
- 대용량 데이터를 한 번에 메모리에 올리지 않고
- 캐시 크기를 적절히 관리하고
- 메모리 누수를 방지하는 것이 우선이다.
GC 옵션을 조정하는 것은 그 이후에 고려하는 것이 일반적인 순서다.
정리
GC는 메모리 회수 도구이지 성능을 높여주는 마법이 아니다.
실무에서 GC 성능 문제의 상당수는
잘못된 객체 생성 패턴
↓
Young Generation 과부하
↓
Promotion 증가
↓
Old Generation 증가
↓
Full GC 증가
라는 흐름에서 시작된다.
따라서 좋은 GC 튜닝은 JVM 옵션을 바꾸는 것이 아니라
애플리케이션이 GC 친화적으로 동작하도록 만드는 것에서 출발한다.
다음 글에서는 G1 GC를 다룬다.
왜 Java 9부터 기본 GC가 되었는지, CMS를 대체한 이유, Region 구조, Mixed GC, Pause Time 목표까지 실무에서 가장 많이 사용하는 GC를 깊이 있게 살펴본다.
'language > java' 카테고리의 다른 글
| ZGC (Z Garbage Collector) - 초저지연 GC의 원리 (0) | 2026.06.29 |
|---|---|
| G1 GC (Garbage First GC) (0) | 2026.06.29 |
| Escape Analysis 재방문 (Scalar Replacement, Lock Elision) (0) | 2026.06.29 |
| 객체 생성 비용 줄이기 (Object Creation Cost) (0) | 2026.06.29 |
| Spring MVC 예외 처리 내부 구조 (0) | 2026.06.24 |
댓글