가비지 컬렉터 (2) - CMS

블러거·2026년 1월 7일

JVM

목록 보기
8/26

CMS

CMS 는 가비지 컬렉션 일시 중지 시간을 최소화하고, 애플리케이션 실행 중에 가비지 컬렉터와 프로세서 리소스를 공유할 수 있는 애플리케이션을 위해 설계되었습니다. 일반적으로 장기간 보존되는 데이터(tenured 크기가 큰 경우)가 비교적 많고 프로세서가 두 개 이상인 시스템에서 실행되는 애플리케이션은 이 컬렉터를 사용하면 이점을 얻을 수 있습니다. 하지만 일시 중지 시간이 짧은 모든 애플리케이션에 이 컬렉터를 고려해 볼 수 있습니다.
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html

CMS 는 STW 에 의한 정지 시간을 최소화시키는 컬렉터다. Concurrent-Mark-Sweep 의 약자다.
STW 정지 시간을 최소화하는 방법이 뭘까. 이름에서 힌트를 얻자면 '동시에' 한다는 것이다.

Garbage Collection 단계에서 STW 를 일으키는 단계는 크게 두가지다.
1. Marking : 살아있는 객체를 마킹하는 과정에서 객체 그래프가 변경되면 안된다.
2. Compact : 살아남은 객체의 메모리 파편화를 막기 위해 한 곳으로 모으는 과정이다. 객체의 위치를 옮겨야 하므로 애플리케이션을 멈추고 진행한다.

CMS 는 꼭 필요한 부분을 남기고 많은 부분을 STW 없이 유저 스레드와 동시에 진행하는 방법으로 일시 정지 시간을 줄였다.

CMS 특징

  • Old Gen 용 GC.
    • -XX:+UseConcMarkSweepGC 로 활성화. Young Gen GC 는 ParNew 가 사용된.
  • Mark-Sweep 알고리즘 기반
  • JDK 14 부터는 없어져 사용할 수 없음
    • 대체제는 G1 GC
  • 지연 시간 최소화 컬렉터라 서버용 빠른 응답을 하기에 적절하다.

없어진걸 왜 알아야 하나..

서버용 JVM 은 지연 시간을 줄이는 목표로 발전했다. 최근의 그러한 Garbage Collector 가 ZGC 이다.

지연 시간을 줄이는데 가장 크게 발목 잡는 것이 STW 이고, STW 를 줄이는 방법은 객체의 생성/삭제/이동을 유저 스레드의 동작과 안전하게 같이 진행하는 것이다.

CMS 이전의 GC(Serial, ParNew, PS..) 는 GC 모든 과정에서 STW 를 하였다. CMS 는 유저 스레드와 GC 의 일부를 동시에 실행시킨 첫 GC 로서 의미가 있으며, 어떻게 그렇게 했는지를 이해하면면 CMS 이후에 나온 GC 를 이해하기 더 쉽다.

Garbage Collection 중 동시에 실행하는 것

  1. GC Root 로 부터 참조 체인을 따라가면서 Marking 하는 작업
  2. Sweep

Marking 단계중 CMS 가 유저 스레드와 동시에 실행하는 작업은 GC Root 로 부터 참조 체인을 따라가며 살아있는 객체에 마킹하는 작업이다. 루트 노드를 열거하는 작업은 여전히 STW 로 실행된다.

그러나 루트 노드 열거보다 참조 체인을 따라가는 작업이 훨씬 오래 걸리기에, 의미있는 발전이다.

또한 Compact 가 아닌 Sweep 이라서, 죽은 객체를 회수(Sweep)하는 작업을 유저 스레드와 동시에 진행할 수 있었다.

동작 방식

1. 최초 Marking

STW 루트 노드와 직접 연결된 객체만 mark 한다. 빠르게 끝남.

2. 동시 Marking

NO-STW 루트 노드와 직접 연결된 객체로 부터 객체 그래프를 탐색하며 mark 한다. 새로 생긴 객체는 증분 업데이트 방식에 따라 따로 기록해둔다.

3. Re-Marking

STW. 증분 업데이트 방식에 의해 따로 기록한 객체를 반영한 mark 를 진행한다. 최초 표시보다 정지가 조금 더 길지만, 여전히 짧은 시간이다.

4. 동시 Sweep

NO STW 유저 스레드가 실행되는 것과 동시에 죽은 객체를 회수한다.

CMS 의 단점

1. 프로세서 자원에 민감

유저 스레드와 동시에 GC 를 실행한다는 것은 하나의 시간에 CPU 처리량을 GC 가 뺏어간다는 이야기다.
CMS 는 (프로세서 코어 수 + 3)/4 개의 스레드 로 GC 를 진행한다. 프로세서 코어 수가 적으면 적을수록 GC 가 사용하는 자원의 비용이 커진다.

따라서 Oracle 문서 에서도 프로세서가 2개 이상인 환경에서 적합하다고 명시되어 있다.

2. 동시 모드 실패

CMS 는 유저 스레드와 동시에 실행된다.
따라서 Old Gen 의 공간이 100% 찼을때 GC 를 하면 안된다. 동시에 실행되는 스레드에 의해 만들어질 객체가 할당될 공간이 필요하기 때문이다.

특히 동시에 실행되는 유저 코드에 의해 한 객체가 죽었다 하더라도, 그 시점이 CMS 의 Marking 단계가 끝난 직후라면 다음 GC 까지 기다려야 한다. 이런 객체를 부유 쓰레기라 한다.

-XX:CMSInitiatingOccupancyFraction 옵션은 Old Gen 이 몇 % 차야 CMS GC 가 실행될지 설정할 수 있다. JDK6 부터 기본값은 92% 이다.
Old Gen 의 여유 공간이 8% 남게 되면 CMS GC 가 시작된다.

만약 8% 남은 상태에서 더 큰 공간의 할당 요청이 오면?
이 상황을 동시 모드 실패 라고 하며, CMS 는 애플리케이션을 멈추고 Serial Old GC 로 Major GC 를 실행한다.
이 동작은 매우 느리기 때문에 -XX:CMSInitiatingOccupancyFraction 옵션을 잘 조정하는게 중요하다.

3. 메모리 파편화

mark-sweep 알고리즘의 한계이다. CMS 역시 직면해야하는 문제다.
메모리 파편화가 심해지면 전체 GC 를 실행해야 한다.

JDK 8 까지는 Foreground Collection 이라고 해서 -XX:+UseCMSCompactAtFullCollection, -XX:+UseCMSCompactAtFullCollection 옵션을 활용하여 Full GC 전 Compact 를 하여 파편화로 인한 메모리 부족을 해결할 수 있었다.

하지만 JDK-8010202에서 Foreground Collection 은 전체 GC 보다 더 오래 걸리고 문제가 많아서 JEP-214 에 의해 JDK9 부터는 사라졌다.

그래서 메모리 파편화는 결국 CMS 에서 근본적 문제로 남게 되었다.

profile
안녕하세요!

0개의 댓글