프로그래밍 2010/04/14 12:47

Sun JVM이 지원하는 Garbage Collector들

플랙스 디버깅 환경이나 플래시 GC를 이해하는 데 도움이 되는 내용들임.


Sun JVM이 지원하는 Garbage Collector들

앞서 간략하게 본 것처럼(그리고 널리 알려진 바대로), Sun JVM은 Minor GC(New Generation 정리)와 Full GC(Old Generation 정리)라는 두 종류의 GC를 통해 메모리를 관리한다. 

Minor GC 작업과 Full GC 작업이 모두 성능에 지대한 영향을 미치기 때문에, 이 두 개의 GC 작업을 얼마나 효율적으로 할 것인가가 지속적인 관심과 개선의 대상이 되었다. 이러한 작업의 결과로 Sun JVM은 공식적으로 다음과 같은 세 가지 종류의 GC를 제공하게 되었다.
Default Serial Garbage Collector: 전통적으로 사용되던 Garbage Collector
Throughput Garbage Collector: Throughput 개선에 집중하는 Garbage Collector. Throughput 개선을 위해 New Generation 정리시에 병렬 작업을 수행한다.
Low Pause Garbage Collector: Response Time 개선에 집중하는 Garbage Collector. Response Time 개선을 위해 Old Generation 정리 작업을 Applicaiton Thread를 (최대한) 블로킹하지 않는 방식으로 진행한다.


Default Serial Garbage Collector
-XX:+UseSerialGC 옵션에 의해 활성화된다. JDK 1.4에서는 이 Collector가 기본 설정이다. JDK 1.5에서는 약간의 편법(?)이 사용되는데, 만일 JVM이 실행되는 환경이 Server 급이라면 Throughput Garbage Collector가 기본 설정이 된다.

(참조)
Server 급 환경이란? Sun이 설정한 Server 급 환경은 2장 이상의 CPU, 2G 이상의 메모리를 갖춘 환경을 말한다. 이러다 보니 내 노트북이 Server로 분류되는 황당한 상황이... --; 더구나 JDK 1.5에서는 이 기준으로 Server 환경을 정의하고 Server 환경이 되면 -server -XX:UseParalleGC -Xms1024M 같은 옵션을 기본으로 적용한다. 이걸 두고 "인공공학적" 설계라고 부르는데... 
사실 좀 장난같다는 느낌이다.

비록 Server급 환경이라도 가용할 수 있는 자원이 많지 않다면 Serial Garbage Collector를 사용하는 것이 일반적으로 바람직하다는 사실에 특히 주의해야 한다. 기계적으로 원칙을 적용해서는 안된다.

Throughput Garbage Collector(혹은 Parallel GC)
-XX:+UseParallelGC 옵션에 의해 지정된다. Throughput Garbage Collector는 New Generation에 의한 Minor GC 작업을 여러 개의 Thread(CPU의 개수만큼)를 이용해 Parallel하게 진행한다. 

아 래 내용은 Sun JVM에서 -XX:+UseParallelGC 옵션을 지정한 상태에서 생성한 Thread Dump 결과의 일부로, CPU 개수와 동일한 2개의 Garbage Collector Thread가 활성화된 것을 확인할 수 있다.

"GC task thread#0 (ParallelGC)" prio=6 tid=0x00aa67e8 nid=0x8c8 runnable
"GC task thread#1 (ParallelGC)" prio=6 tid=0x00aa7230 nid=0x13a4 runnable

Server 급 머신에서는 이 Collector가 기본 설정이라는 것은 의미하는 바가 크다. 특히 CPU 자원이 풍부한 환경에서는 큰 효과를 얻을 수 있다. 

이 Collector를 선택하는데 있어 주의할 점이 하나 있다. 비록 여러 장의 CPU를 갖춘 Server 급 머신이라고 하더라도 다른 프로세스들과 공유해야 환경이라면 이 Collector를 사용하는 것을 바람직하지 않다. 기본적으로 CPU를 충분히 활용하지 못하는 환경이라면 잦은 Context Switching으로 인해 오히려 성능이 저하되기 때문이다. 

-XX:ParallelGCThreads 옵 션을 이용하면 Parallel Collector Threads의 개수를 조절할 수 있다. 만일 모든 CPU를 충분히 사용하지 못하는 환경이라면 GC Thread의 개수를 줄임으로써 효과적으로 Parallel GC 작업을 할 수 있다.

Low Pause Garbage Collector(혹은 CMS GC)
-XX:+UseConcMarkSweepGC 옵션에 의해 활성화된다. Throughput Garbage Collector와는 달리 Response Time를 최적화하게끔 동작한다. 정확하게 말하면 Response Time을 최소화하기 위해 Full GC 과정을 되도록이면 Thread 블로킹없이 진행한다. Thread의 정지 시간(Pause Time)이 최소화되기 때문에 Low Pause Garbage Collector라는 이름이 붙었다.

Low Pause Garbage Collector는 Full GC 과정에서 오는 Thread Blocking을 최소화하기 위해 다단계의 전략을 사용한다. 아래 내용은 Low Pause Garbage Collector를 사용하는 환경에서 GC Dump를 수행한 내용 중 일부를 발췌한 것으로 Collector의 작업 순서를 알 수 있다.

[GC [1 CMS-initial-mark: 128837K(253952K)] 128919K(262080K), 0.0001592 secs]
[CMS-concurrent-mark-start]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs]
[1 CMS-remark: 138222K(253952K)] 145527K(262080K), 0.0232208 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.086/0.199 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.006/0.006 secs]

위의 과정은 다음과 같이 요약 설명할 수 있다.
initial mark(Pause 발생)-->
concurrent mark(Pause 없음. 하지만 Collector와 Application Thread가 CPU를 경쟁)-->
remark(Pause 발생)-->
concurrent sweep(Pause 없음. 하지만 Collector와 Application Thread가 CPU를 경쟁)
즉, Pause를 최소화하면서(inital mark/remark 과정에서만 Pause 발생) 최대한 Collector Thread와 Application Thread가 동시에 작업을 진행하도록 하는 것이 이 Collector 알고리즘의 핵심이다. Full GC를 Concurrent하게 진행하기 위해 initial mark 작업은 Old Generation이 꽉 차기 전에 시작된다. 디폴트로 Old Generation이 68% 이상 점유되면 Full GC 작업이 시작된다(이 값은 -XX:CMSInitiatingOccupancyFraction 값을로 변경 가능하다. 또한 스스로 메모리 사용량을 모니터링하면서 이 값은 동적으로 변경되기도 한다). 반면 Serial Collector와 Throughput Collector는 Old Generation이 꽉 차야만 Full GC를 시작한다.

Pause Time을 최소화함으로써 Response Time은 최적화되지만, Collector Thread와 Application Thread가 CPU 경쟁을 벌임으로써 전반적으로 Throughput, 즉 일량은 감소하는 부작용이 있다는 사실을 기억해야 한다.

한 가지 재밌는 것은 Low Pause Garbage Collector가 비록 Full GC, 즉 Old Generation에 대한 정리 작업을 최적화하는 기법이지만 New Generation에 대한 Minor GC 작업시에도 여러 개의 Thread를 사용함으로써 성능 개선을 꾀한다는 것이다. 이것은 JDk 1.4에서 실험적으로 고안되었고 JDK 1.5에서는 CPU가 여러 장이라면 기본적으로 Parallel GC를 사용한다. 이것은 -XX:+UseParNewGC 옵션으로 결정된다. 
아래 내용은 Low Pause Garbage Collector를 사용하는 환경에서 생성한 Thread Dump의 일부로 New Generation 정리를 위해 여러 개의 Thread가 활성화되어 있는 것을 확인할 수 있다.

"Gang worker#0 (Parallel GC Threads)" prio=10 tid=0x0003bb68 nid=0x1338 runnable
"Gang worker#1 (Parallel GC Threads)" prio=10 tid=0x0003bda8 nid=0x594 runnable

Serial Collector, Throughput Collector(Parallel Collector)와 Low Pause Collector(CMS Collector)는 서로 호환되지 않는다. 가장 이상적인 것은 런타임에 Collector를 선택할 수 있는 것이겠지만 불가능하다. 따라서 시스템의 성능(Client/Server)과 요구 사항(Throughput/Response)에 따라 최적의 Collector를 선택할 수 있도록 해야 한다.

CMS Collector를 사용할 때 한가지 유의할 점은 Heap의 크기이다. CMS Collector는 다른 Collector와는 달리 Full GC를 소극적으로 수행한다. 또한 메모리 Compaction을 적극적으로 하지 않는다. 이런 이유 때문에 메모리 요구량이 많은 경우에는 문제가 생길 수 있다. 따라서 CMS Collector를 사용할 경우에는 Heap 크기를 좀 더 크게 지정해야 한다. 보통 20% 이상의 크기를 추가로 부여할 것을 권고한다.

---(참조)----------------------------------------------------------------------------------------------------------
CMS Collector를 사용할 경우 Permanent Space에 대한 GC 작업을 수행하지 않는다. 따라서 많은 수의 Class를 다루는 Application에서는 Permanent Space에서 OutOfMemory 에러가 날 수 있다. 이 경우에는 다음 두 개의 옵션을 활성해주어야 한다.

-XX:+CMSPermGenSweepingEnabled
-XX:+CMSClassUnloadingEnabled
----------------------------------------------------------------------------------------------------

CMS Collector를 사용할 때 또 한가지 주의할 점은 Mark and Sweep GC가 일어나지 않도록 하는 것이 다. CMS Collector는 Old 영역의 사용률을 보고 적당한 시점에 Concurrent한 GC 작업을 시작한다. 만일 이 작업의 시작 시간이 메모리 요구량에 비해서 느리면 Application Thread가 메모리를 필요할 때 할당받지 못하게 되어서 Mark and Sweep GC를 수행하게 된다. 이렇게 되면 Pause Time을 줄이고자 하는 목적의 달성이 어려워진다. Concurrent GC 작업의 시작은 다음 옵션으로 제어된다.

-XX:CMSInitiatingOccupancyFraction=<1~100의 값>

만 일 이 값이 60이면 Old 영역의 60%가 사용되는 시점에 Concurrent GC 작업이 시작된다. 만일 이 값이 너무 크면 제 때 GC가 이루어지지 못해 Mark and Sweep GC가 작동할 확률이 높아진다. 반면 이 값이 너무 작으면 Concurrent GC 작업이 너무 일찍 시작해서 Applicaiton Thread의 CPU 점유를 방해한다. 따라서 처리량(Throughput)이 낮아질 수 있다. 


중요한 VM 옵션들
Sun JVM의 Heap 및 GC 관리 정책을 결정하는 옵션들은 매우 많다. 너무 많아서 어떤 것을 사용해야 할 지 결정하기가 어려울 정도이다. 어떤 옵션들은 문서화가 거의 되어 있지 않아서 정확한 의미를 이해하기 어려운 것들도 있다.(마치 오라클의 Hidden Parameter 처럼) 아래의 몇 가지 옵션들이 경험적으로 그리고 문서로 정의되고 검증된 것들이다. Heap과 GC의 최적화를 달성하려면 이들 옵션들의 정확한 의미를 이해할 수 있어야 한다.

-Xms: Heap의 초기 크기를 지정한다.
-Xmx: Heap의 최대 크기를 지정한다. 서버용 프로그램에서는 -Xms와 -Xmx를 동일하게 부여함으로써 동적인 메모리 관리의 부담을 덜어주어야 한다.
-Xss: Thread의 최대 Native Stack 크기를 지정한다. 만일 Thread의 개수가 지나치게 많다면 이 크기를 줄여줄 필요가 있다.
-Xoss: Thread의 최대 Java Stack 크기를 지정한다. HotSpot JVM에서는 Thread에 대해 Native Stack만을 사용하기 때문에 이 옵션은 무의미한다.
-XX:NewRatio: New Generation과 Old Generation의 크기 비율이다. 만일 NewRatio 값이 2 이면 New Generation:Old Generation = 1:2 가 된다. 이 값의 경험적 기준치는 8~2 사이의 값을 사용하는 것이다.
-XX:NewSize: New Generation의 크기를 직접 지정한다.
-XX:MaxNewSize: New Generation의 최대 크기를 지정한다. Sun JVM은 New Generation의 크기를 상황에 따라 동적으로 변경(Adaptive)한다. NewSize와 MaxNewSize를 동일하게 하면 크기를 고정할 수 있다. 또는 -XX:-UseAdaptiveSizePolicy 옵션을 지정해도 된다.
-XX:SurvivorRatio: Survivor Space와 Eden Space의 크기 비율을 지정한다. 가령 이 값이 6 이면 (To + From) : Eden = 1:1:6 이 된다. 즉 From Space의 크기가 Eden Space의 1/8이 된다. 경험적으로 추천되는 값은 -XX:NewRatio=2, -XX:SurvivorRatio=6 정도의 값을 사용하는 것이다.
-XX:+UseAdaptiveSizePolicy: Adaptive하게 New Generation의 크기가 Survivor Space의 크기를 변경할 것인지의 여부를 지정한다. 
-XX:PermSize: Permanent Space의 최초 크기를 지정한다. 
-XX:MaxPermSize: Permanent Space의 최대 크기를 지정한다. 매우 많은 수의 클래스를 로딩하는 경우에는 이 값을 늘려주어야 한다.
-XX:+AggressiveHeap: Heap 사용률을 최대한까지 확장한다. 이 옵션을 활성화하면 Heap은 3850M까지, Thread의 Allocaiton Buffer를 256K까지 확대가능하다. 이 옵션을 지정하면 GC 작업도 Parallel하게 진행된다. 만일 서버급 머신을 하나의 JVM이 사용하는 환경이라면 이 옵션만으로도 적절한 성능을 보장할 수 있다. 개인적으로 이 옵션보다는 개별 옵션을 일일이 지정하는 것이 더 바람직하다고 본다.

-XX:+UseSerialGC: Serial Collector를 활성화한다.
-XX:+UseParallelGC: Throughtput Collector(Parallel Collector)를 활성화한다.
-XX:+UseConcMarkSweepGC: Low Pause Collector(CMS Collector)를 활성화한다.
-XX:+UseParNewGC: CMS를 사용할 경우 Minor GC를 Parallel하게 할 지의 여부를 지정한다. CPU가 복수개일 경우는 기본적으로 이 모드를 사용한다.
-XX:+DisableExplicitGC: System.gc() 호출을 통한 강제 GC 작업을 Disable한다.

-verbose:gc : GC 관련 정보를 콘솔에 출력한다.
-xloggc:: GC 관련 정보를 특정 파일에 출력한다.
-XX:+PrintGCDetails: 상세한 GC 정보를 출력한다.
-XX:+PrintGCTimeStamps: GC 발생 시간 정보를 기록한다.
-XX:+PrintGCApplicationStoppedTime: GC로 인해 Application이 정지된 시간 정보를 기록한다.
저작자 표시 비영리 동일 조건 변경 허락