Java Garbage Collector 에러처리 경험

이규훈·2025년 10월 19일

에러 수정 경험

목록 보기
1/1
post-thumbnail

🚨 GC 관련 OutOfMemoryError 분석기 (실제 사례 공유)

최근 업무 중 GC 관련 에러를 경험하여 이를 공유합니다.
개발 서버에서 배포 도중 스프링 애플리케이션이 비정상 종료되는 현상이 발생했고,
원인은 자바 힙 메모리(Heap Memory) Overflow였습니다.

당시 스프링은 외장 톰캣을 통해 실행되는 구조였으며,
에러 원인은 env 파일의 힙 메모리 설정값이 너무 작았던 것이었습니다.
과거에는 충분했던 용량이, 트래픽 증가로 인해 더 이상 감당되지 못한 것이죠.


☕ JVM 메모리 구조 이해하기

🧠 힙 메모리(Heap Memory)
• Java 객체들이 저장되는 공간
• 요청이 들어올 때마다 객체 생성 → 메모리 사용 증가
• 비유하자면 ‘창고’입니다.
• 물건(객체)을 계속 쌓아두다가
• 창고가 꽉 차면 OutOfMemoryError가 발생합니다.


🗑️ GC (Garbage Collection)란?

GC는 “쓰레기 수거차”와 같습니다.

  1. 객체 생성 (물건 쌓임)
  2. 메모리 가득 참
  3. GC 실행! (청소부 출동)
  4. 사용하지 않는 객체 제거
  5. 메모리 확보 완료!

GC 종류
• Young GC: 젊은 객체 영역만 청소 (빠르고 자주 실행)
• Full GC: 전체 영역 청소 (느리지만 확실)


📸 힙 덤프(Heap Dump)

메모리의 ‘사고 현장 사진’입니다.

OutOfMemoryError 발생 순간 → 객체 상태를 파일로 저장
→ 예: java_myrsmadm-svr-12_pid_20251017_110630.hprof
→ 이후 분석 도구로 열어 "어떤 객체가 메모리를 많이 잡고 있었나?" 확인


🕒 에러 발생 타임라인

1️⃣ 정상 상태

Heap: 1024MB 중 약 400MB 사용
[████████░░░░░░░░░░░░░░░░░░░░] 40%

2️⃣ 트래픽 증가

Heap: 1024MB 중 약 658MB 사용
[█████████████████░░░░░░░░░░░] 64%
→ Young GC가 10초마다 발생

3️⃣ 메모리 부족 조짐

Young 영역: 정상적으로 비워짐 ✅
Old 영역: 계속 증가 ⚠️
→ 메모리 누수 의심

4️⃣ OutOfMemoryError 발생 (11:06:33)

Old Generation 가득 참 → 새 객체 생성 불가
→ java.lang.OutOfMemoryError
→ 힙 덤프 생성
→ Tomcat 크래시 💥

5️⃣ 재시작 실패

이미 죽은 프로세스의 PID가 남아있음 → "이미 실행 중입니다" 착각
→ 좀비 프로세스 발생 🧟

6️⃣ DB 연결 실패

재시작 후 HikariCP가 DB 연결 시도
→ PostgreSQL 연결 타임아웃
→ SocketTimeoutException 발생
→ 인증 실패 (401)


⚙️ 문제 요약

구분 원인 결과
💀 OutOfMemoryError 힙 1GB 설정 → 부족 Tomcat 크래시
🧟 좀비 프로세스 비정상 종료 후 PID 잔존 재시작 실패
🔌 DB 연결 실패 네트워크/DB 지연 인증 실패


❓ “GC가 제대로 동작 안 한 거 아닌가요?”

좋은 질문입니다.
사실 GC는 정상적으로 동작했습니다.
다만, “Old Generation이 비워지지 않은 것”이 핵심 문제였습니다.


📊 GC 로그 분석

481초: GC 실행 → Heap 658.5M → 46.6M
492초: GC 실행 → Heap 658.7M → 46.7M
502초: GC 실행 → Heap 658.6M → 46.6M

즉,
• Young 영역(Eden): 완벽하게 비워짐 ✅
• Old 영역: 그대로 남음 ❌
→ Old Generation이 비워지지 않음 → 결국 메모리 부족 발생


🧩 원인 분석

1️⃣ Full GC가 실행되지 않음
• 로그상 Full GC가 단 한 번도 발생하지 않음
• Young GC만 계속 반복 (Old 영역은 청소하지 않음)

G1GC의 작동 방식

  1. Young GC로 버팀
  2. Old 영역이 일정 수준 차면 Full GC 준비
  3. 하지만 Full GC 실행 전에 메모리 한계 도달 → OOM 발생

설정 예시:

-XX:InitiatingHeapOccupancyPercent=35

→ 힙의 35% 차면 Full GC 준비,
하지만 실제로는 Old가 40%인데도 Full GC가 일어나지 않았음.


2️⃣ 메모리 누수 가능성

GC가 Old 객체를 지우지 못하는 이유 중 하나는,
아직 참조 중인 객체가 있기 때문입니다.

// 나쁜 예시 1: 캐시 누수
private static Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value); // 계속 쌓이기만 함!
}

// 나쁜 예시 2: Connection 미닫기
Connection conn = getConnection();
// ...
// conn.close(); 누락 시 누수 발생


💡 요약: GC는 열심히 일했지만…

구분 동작 결과
Young GC 자주 발생, Young 영역 완벽 청소 ✅ 정상
Old GC (Full GC) 실행 안 됨 ❌ Old 영역 누적
힙 크기 1GB (너무 작음) ⚠️ 빠르게 OOM
메모리 누수 참조 유지로 회수 불가 ⚠️ 누수 가능성 높음


🏚️ 비유로 이해하기

집(힙 메모리) = 방 2개

  • 거실(Young Gen): 매일 청소 (Young GC)
  • 창고(Old Gen): 가끔 청소 (Full GC)

문제:
1. 거실은 깨끗하지만,
2. 창고로 계속 물건이 옮겨짐,
3. 창고 청소를 안 함,
4. 결국 집 전체가 터져버림 💥


🔧 해결 방법

1️⃣ 메모리 증설

-Xms1024m -Xmx1024m → -Xms2048m -Xmx2048m

2️⃣ Full GC 조건 조정

Full GC를 더 자주 수행하도록

-XX:InitiatingHeapOccupancyPercent=35 → 45

3️⃣ 메모리 누수 점검

힙 덤프 파일 분석 → 메모리 많이 차지한 객체 확인 → 코드 수정


✅ 결론

GC가 제대로 동작하지 않은 게 아닙니다.
오히려 열심히 일했지만 감당할 수 없었던 상황이었습니다.

핵심 요약:
1. 힙 크기가 너무 작았고,
2. Full GC 타이밍이 늦었으며,
3. Old Generation에 객체가 계속 쌓였고,
4. 일부는 메모리 누수 가능성이 존재했습니다.

👉 즉, GC의 한계 + 설정 미비 + 누수가 복합적으로 작용한 사례였습니다.


💬 GC 관련 에러는 단순한 “메모리 부족” 이상의 의미를 갖습니다.
GC 로그와 힙 덤프를 함께 분석하면, 문제의 본질을 훨씬 정확히 파악할 수 있습니다.


JVM 구조

profile
개발취준생

0개의 댓글