포스트

Java의 Garbage Collection을 어떻게 이해해야 할까

Java의 Garbage Collection은 흔히 “사용하지 않는 객체를 자동으로 지워주는 기능” 정도로 설명된다. 이 설명 자체는 틀리지 않지만, 실무에서 중요한 것은 GC가 있다는 사실보다 GC가 언제 비용이 되고, 어떤 기준으로 동작하며, 왜 튜닝이 필요한지를 이해하는 것이다.

GC의 기본 역할

GC의 목적은 더 이상 사용되지 않는 객체가 차지하고 있는 힙 메모리를 회수하는 것이다.

개발자가 직접 free()를 호출하지 않아도 되기 때문에 메모리 해제를 빠뜨리는 문제는 줄어든다. 대신 어떤 객체를 언제 회수할지는 JVM이 결정한다.

즉, Java는 수동 메모리 해제를 없애는 대신 자동 관리 비용을 도입한 셈이다.

어떤 객체가 “사용되지 않는다”고 판단하나

Java GC는 보통 도달 가능성(reachability) 기준으로 객체 생존 여부를 판단한다.

핵심은 GC Root에서 시작해 참조 그래프를 따라가는 것이다.

GC Root의 예:

  • 현재 실행 중인 스레드의 스택 변수
  • static 필드
  • JNI 참조

이 루트들로부터 도달할 수 없는 객체는 더 이상 사용되지 않는다고 보고 회수 대상이 된다.

즉, “변수가 null이 되었다”는 사실 자체보다, 루트에서 더 이상 닿을 수 없는가가 더 중요하다.

왜 세대별로 나누나

Java GC의 중요한 관찰 중 하나는 대부분의 객체가 오래 살아남지 않는다는 점이다.

예:

  • 요청 처리 중 잠깐 생성되는 DTO
  • 문자열 조합 과정에서 생기는 임시 객체
  • 컬렉션 내부의 짧은 생명 객체

그래서 JVM은 힙을 보통 다음처럼 나눈다.

  • Young Generation
  • Old Generation

새로 생성된 객체는 Young에 들어가고, 여러 번 살아남은 객체만 Old로 이동한다. 이렇게 하면 자주 죽는 객체를 더 효율적으로 수집할 수 있다.

Minor GC와 Full GC를 구분해서 봐야 한다

Young 영역을 주로 수집하는 GC는 보통 Minor GC라고 부른다. 이 작업은 상대적으로 짧고 빈번할 수 있다.

반면 Old 영역까지 깊게 건드리는 수집은 훨씬 비쌀 수 있다. 흔히 Full GC라는 표현으로 많이 부르는데, 이 상황이 잦아지면 응답 지연과 처리량 저하가 눈에 띄게 커진다.

실무에서는 단순히 “GC가 발생했다”보다, 다음을 구분해 보는 것이 중요하다.

  • Minor GC가 잦은가
  • Old 영역 점유율이 빠르게 오르는가
  • Full GC 또는 장시간 pause가 발생하는가

Stop-the-World가 왜 중요한가

GC는 내부적으로 객체 그래프를 안전하게 보기 위해 애플리케이션 스레드를 멈추는 구간을 가진다. 이를 흔히 Stop-the-World라고 부른다.

이 시간이 길어지면:

  • API 응답 지연
  • 배치 처리량 저하
  • 지연 시간 분포의 tail 증가

같은 문제가 발생한다.

그래서 GC 알고리즘의 차이는 결국 다음 질문으로 귀결된다.

처리량을 더 중시할 것인가, pause 시간을 더 줄일 것인가

대표적인 GC를 어떤 관점으로 봐야 하나

Parallel GC

처리량을 중시하는 쪽에 가깝다. 배치성 작업처럼 pause가 어느 정도 허용되는 환경에서는 여전히 유효할 수 있다.

G1 GC

대부분의 범용 서버 환경에서 무난한 선택이다. 힙을 region 단위로 나누고, 비교적 예측 가능한 pause time을 목표로 한다. 현재는 기본값으로 많이 쓰인다.

ZGC, Shenandoah

매우 짧은 pause time이 중요한 환경에서 고려할 수 있다. 대신 메모리 사용 패턴과 JVM 버전, 운영 환경을 함께 봐야 한다.

즉, 특정 GC가 항상 더 좋다고 볼 수는 없다. 시스템 요구사항이 다르기 때문이다.

GC를 볼 때 흔히 놓치는 것

GC 문제는 종종 GC 알고리즘 자체보다 객체 생성 패턴에서 시작된다.

예:

  • 불필요한 객체를 과도하게 생성
  • 큰 컬렉션을 장시간 유지
  • 캐시가 과하게 커짐
  • 문자열과 버퍼를 계속 새로 만듦

이 경우 GC를 바꾸기 전에 애플리케이션이 어떤 객체를 얼마나 오래 잡고 있는지부터 봐야 한다.

즉, GC 튜닝은 JVM 옵션만의 문제가 아니라 코드 구조 문제이기도 하다.

로그에서 먼저 봐야 할 것

GC 로그를 본다면 처음부터 모든 수치를 다 보려고 하기보다 다음을 먼저 보는 편이 낫다.

  • pause 시간이 얼마나 긴가
  • Young 수집이 얼마나 자주 발생하는가
  • Old 영역 점유율이 계속 증가하는가
  • Full GC가 발생하는가

이 네 가지를 보면 대부분의 방향이 나온다.

정리

Java의 Garbage Collection은 자동 메모리 관리 기능이지만, 비용이 없는 마법은 아니다.

  • 도달 가능성을 기준으로 객체를 회수하고
  • 세대별 가설을 활용해 효율을 높이며
  • 때로는 애플리케이션을 멈추고 작업을 수행한다

그래서 GC를 이해한다는 것은 단순히 알고리즘 이름을 외우는 것이 아니라, 내 애플리케이션의 객체 생명주기와 pause 비용을 함께 보는 것에 가깝다.

실무에서는 GC 자체보다 먼저 이런 질문이 유효하다.

  • 객체를 너무 많이 만들고 있지 않은가
  • 오래 살아남는 객체가 무엇인가
  • pause 시간 목표가 무엇인가

이 질문에 답할 수 있어야 GC 로그도 읽히고, 튜닝도 의미가 생긴다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다