유용한 JVM 플래그들 – Part 7 (CMS Collector)
HotSpot JVM의 CMS 컬렉터(The Concurrent Mark Sweep Collector)는 주요한 목표중에 하나를 가진다: 낮은 애플리케이션 일시정지 시간. 이 목표는 웹 애플리케이션과 같은 대부분의 상호작용 애플리케이션들에게 중요하다. 관련된 JVM 플래그들을 살펴보기전에, 짧막하게 CMS 컬렉터 운영과 이것을 사용할때에 부닥치게될 주요 이슈들에대한 요점을 다룰것이다.
처리량 컬렉터와 같이, CMS 컬렉터는 old generation 에서 객체들을 다루지만 그 운영방식은 훨씬더 복잡하다. 처리량 컬렉터는 늘 애플리케이션 쓰레드들을 잠시 멈추게하지만, 아마도 적지않은 시간, 그 시간을 애플리케이션이 안전하게 무시할수 있도록 한다. 그와 대조적으로, CMS 컬렉터는 대부분 애플리케이션 쓰레드들과 동시적으로 실행되도록 디자인되었고 아주 적은(혹은 짧은) 잠시 정지시간만 발생시킨다. 응용프로그램과 동시에 GC를 실행하는 단점은 다양한 동기화와 데이터 불일치 이슈를 발생시킨다. 안전성과 현재 동시성 실행을 당성하기 위해서 CMS 컬렉터의 GC 사이클은 많은 연속된 단계들로 나뉜다.
Phases of the CMS Collector
CMS 컬렉터의 GC 주기는 6단계로 구성된다. 네가지단계는(“Concurrent” 로시작하는 이름을 가진) 실제 애플리케이션에 동시에 실행되는 반면에 두단계는 애플리케이션 쓰레드들이 정지한다.
- 표기 초기화(Initial Mark): 애플리케이션들은 그들의 객체 참조들을 수집하기 위해 잠시 멈춘다. 이것이 끝났을때, 애플리케이션 쓰레드들은 다시 시작된다.
- 동시적 표기(Concurrent Mark): 1단계에서 수집된 객체 참조들로부터 시작해서, 모든 다른 참조되는 객체들까지 하게된다.
- 동시적 전세정(Concurrent Preclean): 두번째 단계가 실행되는 동안 애플리케이션 쓰레드에 의해서 만들어진 객체 참조에서 변경사항은 두번째 단계로부터 결과를 갱신하는데 사용되어진다.
- 다시표기(Remark): 세번째 단계처럼 역시 동시에 발생한다. 게다가 객체 참조에 변화가 발생한다. 따라서, 애플리케이션 쓰레드는 어카운트를 업데이트하는 동안 한번 이상 정지되고 실제 청소작업이 발생되기전에 참조된 객체의 정확한 뷰를 보장한다. 이 단계는 필수적인데, 여전히 참조되는 객체를 수집하는걸 피해야 하기 때문이다.
- 동시적 청소(Concurrent Sweep): 더 이상 참조되지 않는 모든 객체들은 힙에서 제거된다.
- 동시적 리셋(Concurrent Reset): 컬렉터는 다음 GC를 시작할때 클린 상태를 위해서 어떤 집청소 작업(housekeeping work)를 한다.
흔히 잘못 생각하는 것이 CMS 컬렉션은 애플리케이션에 대해서 전체적으로 동시적으로 실행된다고 여기는 것이다. 동시적 단계와 비교했을때 stop-to-world 단계조차도 일반적으로 매우 짧기때문에 그렇지 않다는것을 봤다.
또 주목해야 하는 것이, CMS 컬렉터가 old generation GC에 대해서 대부분 동시적 솔루션을 제공한다 하더라도, young generation GC들은 여전히 stop-to-world 접근법을 사용해 다루어진다. 이렇게하는 이론적 근거는 young generation GC들은 일시 정지 시간이라는게 상호작용 애플리케이션에서조차 만족할만큼 전통적으로 충분히 짧다는 것이다.
Challenges
실제 세계의 애플리케이션에서 CMS 컬렉터를 사용할때, 우리는 튜닝을 위한 요구사항을 만들때에 두가지 주요한 도전에 직면한다.
- 힙 파편화(Heap fragmentation)
- 높은 객체 할당 비율(High object allocation rate)
힙 파편화는 발생 가능한데, 처리율 컬렉터와 다르게, CMS 컬렉터는 단편화(defragmentation)를 위한 어떤 메커니즘도 포함하지 않는다. 결과적으로 총 힙 공간이 꽉찬것과 상관없이 객체할당을 할 수 없는 상황을 애플리케이션 스스로 찾아야할지 모른다. – 이는 객체를 수용하기 위해 이용 가능한 연속 메모리 영역이 없기 때문이다. 이러한 일이 발생하면, 동시성 알고리즘은 더이상 도움이 안되며 따라서, 최후의 수단으로, JVM은 전체 GC(Full Gc)를 발생시킨다. 전체 GC는 처리량 수집기에 의해 사용 된 알고리즘을 실행하는 것을 상기하면 파편화 이슈는 해결된다. – 그것은 또한 애플리케이션을 정지시킨다. 따라서, CMS 컬렉터가 가지고온 모든 동시성 이면에는 여전히 긴 stop-to-world 일시정지가 발생되는 위험이 있다. 이것은 디자인 부분으로(JVM 디자인) 따라서 그것을 바꿀수는 없다. – 우리는 컬렉터를 튜닝함으로써 그 가능성을 줄일 수 있다. 눈에 띄는 stop-to-world 일시정지로부터 100% 안전성을 보장해야만하는 상호작용 애플리케이션에서는 문제가 된다.
두번째 도전은 애플리케이션의 높은 객체 할당 비율이다. 만약 객체 인스턴스화하는 비율이 컬렉터가 힙에서 죽은 객체를 제거하는 비율보다 아주 높다면 동시적 알고리즘은 여러번 실패한다. 어떤점에서, old generation은 young generation 에서 승격된 객체를 수용할 충분히 활용가능한 공간이 없을 것이다. 이러한 상황은 “concurrent mode failure” 라고하며 JVM은 힙 파편화때와 마찬가지로 움직인다. 바로 전체 GC.
이러한 시나로중에 하나가 실제로 일어날때를보면 (공교롭게도 실제 프로덕트 시스템에서도 일반적으로 발생한다), old generation 에 불필요한 엄청난 양의 객체들이 그곳에 존재한다는 것이 판명된다. 한가지 가능한 대책은 old generation 으로 짧게 사는 객체들의 조기 승격을 방지하기 위해 young generation 을 증가시키는 것이다. 다른 해결책은 프로파일러(profiler)을 사용하거나 운영중인 시스템의 힙 덤프를 얻어 과도한 객체할당에 대해서 객체들을 규명하고 애플리케이션을 분석해서 최종적으로는 많은 양의 객체할당을 줄이는 것이다.
다음으로 우리는 CMS 컬렉터 튜닝을 위해 활용가능한 적절한 JVM 플래그들에 대해서 살펴볼 것이다.
-XX:+UseConcMarkSweepGC
이 플래그는 애초에 CMS 컬렉터를 활성화하기 위해 필요하다. 기본적으로, HotSpot 은 처리율 컬렉터를 대신 사용한다.
-XX:+UseParNewGC
CMS 컬렉터를 사용할때, 이 플래그는 다중 쓰레드를 사용해 young generation GC의 parallel 실행을 활성화한다. 아마도 컨셉상 young generation GC 알고리즘에서 사용되는 것과 같기때문에 처리량 컬렉터에서 봤던 -XX:+UseParallelGC 플래그를 다시 사용하지 않는다는데 놀랄 것이다. 하지만, young generation GC 알고리즘과 old generation GC 알고리즘의 상호작용이 CMS 컬렉터에서 다르며 young generation 에서의 구현은 서로 달라 결국 이 둘 플래그는 다른 것이다.
주목할 것은 현재 JVM 버전에서 -XX:+UseConcMarkSweepGC를 지정하면 자동적으로 -XX:+UseParNewGC가 활성화 된다. 결과적으로, 만일 parallel young generation GC가 매력적이지 않다면 -XX:-UseParNewGC 세팅함으로써 비활성화할 필요가 있다.
-XX:+CMSConcurrentMTEnabled
이 플래그를 지정하면, 동시적 CMS 단계는 다중 쓰레드로 동작한다. 따라서 다중 GC 쓰레드는 모든 애플리케이션 쓰레들에서 parallel에서 작동한다. 이 플래그는 이미 기본값으로 활성화된다. 만약 serial 실행이 더 바람직하다면, 하드웨어 사양을 고려했을때, 다중 쓰레드 실행은 -XX:-CMSConcurrentMTEnabled 를 통해서 비활성화될 수 있다.
-XX:ConcGCThreads
-XX:ConcGCThreads=<value> 플래그는 (이전 버전에서 -XX:ParallelCMSThreads 으로 알려진) 동시적 CMS 단계가 동작할때에 사용할 쓰레드 개수를 정의한다. 예를들어, 값이 4면 CMS 주기의 모든 동시적 단계는 4개의 쓰레드를 사용해 동작한다는 뜻이다. 비록 높은 쓰레드의 개수가 동시적 CMS 단계의 속도를 높여줄수 있지만 추가적으로 동기화 오버헤드를 발생시킨다. 따라서 특정 애플리케이션을 다룰때, CMS 쓰레드의 수의 증가가 실제로 성능향상을 가지고 오는지 그렇지 않는지를 측정해야만 한다.
만일 이 플래그를 명시적으로 지정해주지 않으면 JVM은 처리량 컬렉터에서 알려진 -XX: ParallelGCThreads 플래그 값에 기반에 기본값 parallel CMS 쓰레드 수를 계산한다. 사용된 계산 공식은 ConcGCThreads = (ParallelGCThreads + 3)/4 이다. 따라서 CMS 컬렉터에서, -XX:ParallelGCThreads 플래그는 stop-to-world GC 단계에서만 영향을 주는게 아니라 동시성 단계에서도 영향을 준다.
요약을 하자면, CMS 컬렉터 실생에 다중쓰레드 설정을 위한 몇가지 방법이 있다. 이렇게 말하는 이유는, 먼저 CMS 컬렉터의 기본 세팅값을 사용해보고 튜닝이 필요하면 먼저 측정을하도록 권고하기 때문이다. 프로덕트 시스템에서(혹은 프로덕트와 유사한 테스트 시스템에서) 측정이 애플리케이션의 목표로하는 일시 정지 시간에 도달하지 않았다면 이러한 플래그를 통한 GC 튜닝은 고려해볼만 하다.
-XX:CMSInitiatingOccupancyFraction
처리량 컬렉터는 힙이 꽉 찼을때만 GC 주기를 시작한다. 예를들어, 새로운 객체를 할당할 공간이 없거나 객체를 old generation으로 승격시킬 공간이 없을때. CMS 컬렉터에서는 동시적 GC 동안에도 애플리케이션이 동작중여야하기 때문에 오랜시간을 기다리면 안된다. 따라서, 애플리케이션이 out of memory 되기전에 GC 주기를 끝내기 위해서 CMS 컬렉터는 처리량 컬렉터보다 일찍 GC 주기를 시작할 필요가 있다.
서로 다른 애플리케이션이면 객체 할당 패턴도 서로 다르며, JVM은 실제 객체 할당 및 비할당에 대한 런타임 통계를 수집하고 CMS GC 주기를 언제 시작할지 결정할때 사용한다. 이 과정을 부트스랩기, JVM은 첫번째 CMS 실행을 시작할때 힌트를 획득한다. 그 힌트는 -XX:CMSInitiatingOccupancyFraction=<value> 통해서 퍼센트로 old generation 힙 공간의 사용량으로 표시되어 지정될 수 있다. 예를들어 값이 75면 old generation의 75%를 획득했을때에 첫번째 CMS 주기를 시작하라는 의미다. 전통적으로 CMSInitiatingOccupancyFraction 의 기본값은 68 이다. (오랫동안 경험을 통해 얻은 수치다)
-XX+UseCMSInitiatingOccupancyOnly
우리는 런타임 통계에 기반해 CMS 주기를 시작할지 결정하지 않도록 JVM 에게 지시하기 위해 -XX+UseCMSInitiatingOccupancyOnly 를 사용할 수 있다. 대신, 이 플래그가 활성화된 때에, JVM은 첫음 한번만이 아닌 매번 CMS주기에서 CMSInitiatingOccupancyFraction 값을 사용한다. 그러나, 대부분의 경우 JVM이 우리 인간보다 GC 의사결정을 좀 더 잘한다는 것을 염두에 둬야 한다. 따라서, 이것은 합당한 이유가(ex, 측정을 통해 데이터를 얻었을때에) 있을때 뿐만아니라 애플리케이션에 의해서 생성된 객체의 생명주기에대해서 실제로 좋은 지식을 가지고 있는 경우에 이 플래그를 사용해야 한다.
-XX:+CMSClassUnloadingEnabled
처리량 컬렉터와 비교해, CMS 컬렉터는 기본적으로 permanent generation 에 GC를 수행하지 않는다. 만약 permanent generation GC가 필요하다면, -XX:+CMSClassUnloadingEnabled 통해서 활성화될 수 있다. 이전버전의 JVM에서는 추가적으로 -XX:+CMSPermGenSweepingEnabled 플래그 지정이 필요했었다. 주의할 것은, 이 플래그를 지정하지 않는다하더라도, 공간이 부족하게 되면 permanent generation 의 가비지 컬렉트를 시도할 것이지만 컬렉션은 동시적으로 수행되지 않을 것이다. – 대신, 다시 한번, 전체 GC가 동작할 것이다.
-XX:+CMSIncrementalMode
이 플래그는 CMS 컬렉터의 점진적 모드(incremental mode)를 활성화 한다. 점진적 모드는 애플리케이션 쓰레드에 완전히 양도(yield)되도록 동시적 단계를 주기적으로 잠시 멈추게 한다. 결론적으로, 컬렉터는 전체 CMS 주기를 완료시키기 위해서 아주 오랜시간을 가질 것이다. 따라서, 점진적 모드 사용은 일반적인 CMS 사이클에서 동작중인 컬렉터 쓰레드가 애플리케이션 쓰레드와 아주 많이 간섭이 발생되고 있다고 측정되어질경우에 유효하다. 이것은 동시적 GC 수용을 위해 충분한 프로세스를 활용하는 현대 서버 하드웨어에서 아주 드물게 발생된다.
-XX:+ExplicitGCInvokesConcurrent 와 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
오늘날, 가장 폭 널게 받아들여지는 최고의 기술은 애플리케이션에서 System.gc() 호출에 의해서 명시적으로 호출되는 GC를 차단하는 것이다. 이러한 조언이 사용되어지는 GC 알고리즘과 관련이 없다고 생각하고 있는데, CMS 컬렉터를 사용하고 있을때에 시스템 GC는 기본적으로 전체 GC를 발생시키는 아주 않좋은 이벤트이기 때문에 언급하고 넘어간다. 운좋게도, 기본 행동을 바꿀수 있는 방법이 있다. -XX:+ExplicitGCInvokesConcurrent 플래그는 JVM이 시스템 GC 요청이 있을때마다 전체GC 대신 CMS GC를 실행하도록 지시한다. 두번째 플래그인 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 는 시스템 GC가 요청될 경우에 추가로 permanent generation 을 CMSGC에 포함하도록 해준다. 따라서, 이러한 플래그의 사용으로, 우리는 예기치못한 시스템GC의 stop-to-world에 대해서 우리 스스로 보호할 수 있다.
-XX:+DisableExplicitGC
CMS 컬렉터 주제를 다루는 동안, 어떤 타입의 컬렉터를 사용하던지간에 시스템 GC 요청을 완벽하게 무시하도록 JVM에게 지시하게하는 -XX:+DisableExplicitGC 플래그를 언급할 좋은 기회다. 필자에게, 이 플래그는 더 이상 생각할 필요도 없이 모든 JVM 운영에서 안전하게 지정되어질수 있도록 기본 플래그 세트에 포함된다.