백엔드 면접 용어 사전
Java·객체지향STW

Stop-The-World

GC 수행을 위해 애플리케이션 스레드를 전부 멈추는 구간. 지연 시간의 주된 원인이다.

GC를 수행하기 위해 애플리케이션 스레드를 전부 멈추는 시간(STW).

왜 멈춰야 하나

GC가 객체 그래프를 순회하는 도중에 애플리케이션이 참조를 바꾸면 잘못된 판단을 하게 된다.

GC: A를 검사 → "살아 있음" 표시
    B를 검사 중...
앱: 그 사이 A가 참조하던 C를 끊고, D가 C를 참조하게 함
GC: C를 "쓰레기"로 판단하고 회수  ← 실제로는 D가 쓰고 있다  ✗

살아 있는 객체를 회수하면 프로그램이 깨진다. 그래서 최소한 일부 구간에서는 세상을 멈춰야 한다.

안전 지점(safepoint)

아무 때나 멈출 수는 없다. 스레드가 일관된 상태여야 스택을 정확히 읽을 수 있다.

  • JVM이 "멈춰라" 플래그를 세운다
  • 각 스레드는 안전 지점(메서드 호출, 루프 뒤쪽 분기 등)에 도달하면 멈춘다
  • 전원이 멈추면 GC 시작

함정 — 안전 지점이 없는 긴 루프(int 카운터의 단순 루프는 JIT가 안전 지점 검사를 제거하기도 한다)가 있으면, 다른 스레드는 다 멈췄는데 그 하나 때문에 전체가 대기한다. 이것을 "safepoint 도달 지연"이라 하며 GC 시간이 아닌데도 정지가 길어지는 원인이 된다.

무엇이 문제인가

STW 100ms → 그 동안 모든 요청이 응답하지 않는다
          → p99 지연이 100ms 이상 튄다
          → 헬스 체크가 실패해 로드밸런서에서 제외될 수도
          → 분산 락 TTL이 만료돼 정합성이 깨질 수도  ★

★ GC 정지 중에 분산 락이 만료되는 시나리오는 Redlock 논쟁의 핵심이기도 하다. 자기는 락을 갖고 있다고 믿는데 실제로는 만료된 상태로 깨어난다.

수집기별 정지 시간

수집기전형적 STW
Serial / Parallel수백 ms ~ 수 초
G1수십 ms (목표 지정 가능)
ZGC / Shenandoah1ms 미만 (힙 크기와 무관)

ZGC가 어떻게 1ms를 달성하나 — 마킹과 재배치를 애플리케이션과 동시에 수행한다. 객체를 옮기는 중에 앱이 접근하면 로드 배리어가 가로채 새 위치로 자동 전달한다. STW는 루트 스캔 등 극히 짧은 구간만 남는다.

대가는 처리량 약간 감소(배리어 오버헤드)와 메모리 사용 증가다.

줄이는 방법

  • ① 객체를 덜 만든다 ← 가장 근본적
    • 불필요한 박싱, 문자열 결합, 대용량 컬렉션 복사 제거
  • ② 힙 크기를 적절히
    • 너무 작으면 GC가 잦고, 너무 크면 한 번이 길다
  • ③ 저지연 수집기로 교체 (ZGC)
  • ④ Old 승격을 줄인다
    • 요청 처리용 객체가 Old로 올라가면 Full GC를 유발한다
    • Young 영역을 충분히 크게 (-XX:NewRatio)
  • ⑤ 캐시를 힙 밖으로 (오프힙, Redis)

④가 중요하다 — Young이 너무 작으면 아직 처리 중인 객체가 살아남아 Old로 올라간다(조기 승격). Old가 차면 Full GC가 발생한다.

측정

-Xlog:gc*:file=gc.log:time,uptime,level,tags   # 자바 9+
[15.234s][info][gc] GC(12) Pause Young (G1 Evacuation Pause) 512M->128M(1024M) 25.3ms
                              ↑ 정지 종류           ↑ 전→후(전체)      ↑ 정지 시간

봐야 할 것

  • 정지 시간이 목표(보통 100~200ms) 안에 들어오는가
  • Full GC가 자주 나는가 (드물어야 정상)
  • GC 후에도 사용량이 안 줄어드는가 (누수 신호)

GCEasy 같은 도구에 로그를 넣으면 시각화해 준다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Java·객체지향 — OOP·JVM·GC·컬렉션