가비지 컬렉터 (1) - Classic

블러거·2026년 1월 7일

JVM

목록 보기
7/26

Garbage Collector 를 볼 때

Garbage Collector 의 성능은 처리량(Throughput), 지연 시간(Latency), 메모리 사용량(footprint) 세가지 요소로 측정된다. 이 세가지는 모두 만족시키기 어렵다.

Garbage Collector 를 알아볼때에는 각 컬렉터들이 어떤 성능을 중요시하는지 알고 보는게 좋다.

지연 시간

지연 시간은 여러 요소에 의해 바뀌겠지만, Garbage Collector 관점에서는 일시 정지 시간과 관련이 많다. Stop-The-World 라고 하는 일시 정지 시간은 죽은 객체를 판별(mark)하거나, 살아남은 객체를 모으는(compact) 과정에서 필요하다.

이 STW 는 애플리케이션 전체를 멈추게 하기 때문에 P99, P999 같이 튀는 latency 를 유발할 수 있다. 멈춰있는 시간이 길수록, 그 사이에 해야 할 일이 많이 쌓일수록 요청중 튀는 latency 가 발생할 확률이 높다.

따라서 CMS, 셰넌도어, ZGC 같은 경우는 STW 를 최소한으로 줄이기 위한 방법들로 발전해 왔다. 큰 틀에서는 mark, compact 과정을 유저 스레드와 동시에 실행하는 방법을 각 garbage collector 에서 다르게 구현한다.

지연 시간이 짧은 garbage collector 는 유저 대상으로 하는 서버 애플리케이션에서 적절하다. java 언어가 서버 애플리케이션 개발에서 가장 많이 쓰이기 때문에, 최신의 garbage collector 들은 지연 시간을 최소화 하는 방향으로 진화하고 있다.

처리량

처리량은 위와 같다. 사용자 코드 실행 시간이 99초, GC 시간이 1초라면 처리량은 99% 이다.
한정된 CPU 자원에서 GC 를 위해 스레드가 사용되는 비율이 얼마나 되는가를 수치화 한 것이다.
짧은 GC 시간에 많은 양의 객체를 정리할 수 있는 garbage collector 가 처리량이 좋다고 할 수 있다.

보통 짧은 지연 시간을 추구하면 처리량은 낮아진다.

  1. 처리해야 하는 객체는 변하지 않는데 지연 시간을 짧게 목표한다면 한번의 STW 를 짧게 유지해야한다. 그렇게 되면 결국 GC 를 짧게 여러번 호출해야하기 때문에 그만큼 처리량은 낮아진다.
  2. 지연 시간을 짧게 하기 위해 유저 스레드와 동시에 GC 를 실행한다고 했다. 그 만큼 유저 스레드가 CPU 자원을 사용한다는 것이므로 처리량이 낮아질 수 있다.

저지연 컬렉터인 ZGC, 셰넌도어의 목표는 지연 시간을 최소로 함과 동시에 G1 에 비해 처리량의 감소가 10% 미만으로 되게끔 하는 것이었다.

처리량이 높은 GC 는 분석 애플리케이션 등 유저와 상호작용이 적은 애플리케이션에 적합하다.

메모리 사용량

세대 구분 이론에 따른 메모리 layout 을 처리하는 Garbage Collector 들은 세대간 참조를 해결하기 위해 카드 테이블을 관리한다.
이 카드 테이블도 Gacbage Collector 마다 재각각이며, 메모리 사용량이 다르다.

또한, 메모리 사용량은 아니지만 Garbage Collector 들은 카드 테이블 관리에 사후 write barrier 를 사용하거나, ZGC 에서 컬러 포인터 구현에 load barrier 를 쓰는 등 여러가지 barrier 를 사용한다. 이는 애플리케이션에 가하는 실행 오버헤드이며 주시해야 하는 footprint 이다.

세대 구분 이론에 따른 알고리즘 선택


https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
G1 GC 이전의 GC(Serial, Parallel, CMS) 들은 모두 위와 같은 메모리 레이아웃에서 동작했다.

weak generational hypothesis 에 따르면 새로 생성되는 객체중 대다수는 빠르게 죽기 때문에 Young Generation 의 GC 는 메모리 파편화를 방지하며 객체가 적을 때 효율적인 Mark-Copy 알고리즘을 사용한다.

반대로 Old Generation 의 GC 는 큰 영역을 청소하기 때문에 오래걸린다. 최대한 많은 양을 저장하여 GC 횟수를 줄이기 위해 크기의 반만 사용할 수 있는 Mark-Copy 대신 Mark-Sweep 또는 Mark-Compact 를 사용하는게 좋다.

초창기의 JDK 에서는 Garbage Collector 들이 Young/Old Gen 각각용으로 만들어졌고 조합을 하여 사용 가능했다. 시간이 지남에 따라 Garbage Collector 를 Young/Old 각각 설정할 수 있는 것이 아닌 하나만 설정하면 최적의 조합으로 셋팅되는 방식으로 하나로 합쳐졌다.

예를 들어 CMS 는 Old Gen 용 Collector 이다. 하지만 JDK9 부터는 CMS 를 사용하면 Young Gen 용 Collector 로서 Parnew 가 자동으로 선택된다.

이런 역사를 알아놓으면 이해하기 한층 편할 것 같다.

GC 뭐쓰는지 알기

  1. ManagementFactory.getGarbageCollectorMXBeans()
  2. -XX:+PrintGCDetails / -Xlog:gc* (버전에 따라 둘 중 하나)
  3. jhsdb jmap --heap --pid [pid]
  • jps 로 pid 얻을 수 있음.

기본적 컬렉터들

Young Generation Garbage Collector

Serial [-XX:+UseSerialGC]

  • mark-copy
  • 메모리 사용량이 가장 적다.
  • 단일코어, 또는 코어가 적은 환경에서 스레드 상호작용이 없으므로 오버헤드가 없다.
  • DefNew GC 라고도 한다.

파뉴(ParNew) [-XX:+UseParNewGC]

  • mark-copy
  • Serial 과 다른점은 GC 를 병렬로 처리한다는 것이다. 하지만 일반적으로 Parallel Collector 라고 부르는 컬렉터는 파뉴가 아니다. 처리량을 목표로 두고 처리량을 옵션으로 조절할 수 있는 PS 컬렉터가 Parallel 컬렉터다.
  • JDK8 까지는 default GC 가 Parallel 이며 CMS 가 권장되었다. Old 영역의 CMS 는 Young 영역에 PS 를 조합하지 못하였으므로 파뉴가 대신 많이 쓰였다.
    • -XX:+UseConcMarkSweepGC 로 cms 를 사용하면 기본적으로 -XX:+UseParNewGC 가 켜진다. -XX:-UseParNewGC 옵션을 주면 Serial Collector(DefNew young collector) 가 쓰인다.
  • JDK9 부터는 G1 GC 가 default GC 이며 권장된다. -XX:+UseParNewGC 옵션은 사라졌으며, CMS 선택시 파뉴가 Young 영역 컬렉터로 강제된다.
  • 싱글스레드 환경에서는 시리얼GC 보다 성능이 떨어진다.
  • -XX:ParallelGCThreads 로 GC 스레드 수 제한 가능. CMS 쓸때 가능한 옵션.

페러렐 스캐빈지(PS) [-XX:+UseParallelGC]

  • mark-copy
  • 처리량을 제어하는게 목표인 컬렉터.
  • -XX:MaxGCPauseMillis : 정지 시간이 이 시간이 넘지 않도록 노력한다. 이 시간이 너무 적으면 Young 영역의 크기가 적게 할당되어 오히려 처리량이 떨어질 수 있다.
  • -XX:GCTimeRatio : GC 시간이 1% 이상이 되지 않도록 보장. 기본값은 99.
  • -XX:+UseAdaptiveSizePolicy : 가상머신이 성능 모니터링 정보를 수집하여 최적의 정지 시간과 처리량을 보장하기 위해 -Xmn, -XX:SurvivorRatio 같은 값들을 자동 조정해준다.
    • -XX:MaxGCPauseMillis, -XX:GCTimeRatio 만 설정해주고 JVM 이 설정해주는 값을 모니터링하는것도 좋은 방법.

Old Generation Garbage Collector

시리얼 올드 [-XX:+UseSerialGC]

  • mark-compact
  • 코어가 적은 환경에서 주로 쓰인다.
  • 따로 Old Gen 만 이 컬렉터를 지정하는 옵션은 없다. -XX:+UseSerialGC 로 Young/Gen 모두 시리얼로 동작할 때 쓰이는 MarkSweepCompact GC(이름임) 가 이것이다.
  • -XX:+UseParallelGC -XX:-UseParallelOldGC 이렇게 옵션을 주면 Old 영역에 PS MarkSweep 가비지 컬렉터가 쓰인다.
    • PS Mark Sweep GC 는 아래 페러렐 올드에서 쓰이는 병렬 버전과, 지금 설명에서의 조합에서 쓰이는 Serial 버전이 있다.
    • -XX:+PrintGCDetails 로 Old Gen 이름을 보면 -XX:+UseParallelOldGC 는 ParOldGen, -XX:-UseParallelOldGC 는 PSOldGen 으로 표시되는걸 볼 수 있다.
    • JEP-366 에서 JDK 14 에 이 조합은 폐기대상이 되었고, 15 부터는 UseParallelOldGC 옵션 자체가 없어졌다.
  • cms 의 동시 모드 실패시 백업용 컬렉터로도 Serial Old 가 쓰일 수 있다.

페러렐 올드 [-XX:+UseParallelOldGC]

  • mark-compact
  • 멀티스레드로 병렬 회수한다.
  • Young Gen PS 의 Old Gen 버전이다. 처리량이 중요하다면 PS + 페러렐 올드 조합을 사용하면 된다.
  • Old Gen 을 ParOldGen 으로 관리하는 PS MarkSweep 컬렉터를 사용한다.
  • JDK8 의 기본 컬렉터 옵션인 -XX:+UseParallelGC 를 사용하면 Old Gen 에는 이 컬렉터가 자동 선택된다.
  • JDK9 이후 기본 컬렉터는 G1 으로 바뀌었다. 이때부턴 -XX:+UseParallelGC 옵션으로 Young Gen 에는 PS, Old Gen 에는 페러렐 올드를 사용한다.
  • JDK15 부터는 -XX:+UseParallelOldGC 옵션이 없어졌다.(JEP-366)

참고
https://openjdk.org/jeps/366
https://www.fasterj.com/articles/oraclecollectors1.shtml
https://stackoverflow.com/questions/16442934/serial-mark-sweep-compact-psoldgen-ps-stands-for?utm_source=chatgpt.com
https://blog.ragozin.info/2011/12/garbage-collection-in-hotspot-jvm.html
https://stackoverflow.com/questions/39929758/ps-marksweep-is-which-garbage-collector

profile
안녕하세요!

0개의 댓글