태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

[ Enterprise Java는 거대한 동기화 머신이다 - GC ] Enterprise Java & Oracle 성능 분석의 기초 - Part6

Enterprise Java 2007.08.30 17:42
Enterprise Java는 거대화 동기화 머신이다.

Sun HotSpot JVM

Heap Management & Garbage Collection
(아래 사실들은 이미 널리 알려진 것들이지만 정리하는 의미에서 간략하게 기술한다)
Sun HotSpot JVM(이후 Sun JVM)은 전통적으로 Generation에 기반한 Heap 관리 방법을 사용한다. Generation, 즉 세대에 기반한다는 것은 오브젝트의 수명에 따라 메모리를 관리한다는 의미다.

Garbage Collection 알고리즘의 개발자들은 통계학적으로 매우 중요한 사실 하나를 발견했다. 그것은 대부분의 오브젝트들이 매우 빨리 죽는다(Short-lived)는 것이다. 즉, 수명이 짧고 덩치가 작은 많은 수의 오브젝트와 수명이 길고 덩치가 큰 적은 수의 오브젝트들이 메모리를 사용한다.

여기서 얻은 통찰력을 기반으로 Generation에 기반한 GC 알고리즘이 등장하게 되었다. 이 알고리즘의 핵심은 어차피 빨리 죽을 오브젝트들은 빨리 메모리에서 밀려나도록, 오래 남을 오브젝트들은 되도록이면 밀려나지 않도록 별도의 공간을 마련해준다는 것이다. Generation 기반의 GC에서는 다음과 같은 방법으로 메모리를 관리한다.
  • 메모리 영역을 New Generation(혹은 Young Generation, Nursery)과 Old Generation(Tenured Generation)으로 나눈다. 즉 어린 오브젝트들이 사는 공간과 나이가 많은 오브젝트들이 사는 공간으로 분리한다.
  • New Generation은 다시 Eden(혹은 Allocate)와 Survivor Space로 나뉜다. Eden은 말 그대로 최초로 태어난(Allocate된) 오브젝트들이 거주하는 공간이다. Eden이 꽉 차면, 죽은 오브젝트들을 정리하고 살아남은 오브젝트들은 Survivor Space(생존자들을 위한 공간)로 옮겨진다. 이 과정에서 흔히 Minor GC으로 불리는 작업이 발생한다.(Survivor Space는 메모리 작업의 효율성을 위해 To Space/From Space라는 두 개의 공간으로 관리된다)
  • Survivor Space가 꽉 차면, 다시 살아남은 오브젝트들은 Tenured Space(종신권을 가진 자들을 위한 공간)으로 옮겨진다.
  • 만일 Tenured Space가 꽉 차면 비상사태가 발생한다. 인구폭발로 인해 더 이상 거주 공간이 없는 상태가 되기 때문이다. 이렇게 되면 JVM은 모든 Thread를 블로킹시키고 Full GC(혹은 Major GC)를 수행한다. 모든 오브젝트들에 대해 생존 여부를 조사해서 죽은 오브젝트들을 실제로 메모리에서 밀어낸다.
  • 이 과정에서 필요하면 메모리 공간을 더 키운다. 확장 가능한 최대 메모리 공간은 -Xmx 파라미터에 의해 지정된다.
  • 이 모든 과정을 수행하고도 메모리를, 즉 오브젝트들의 거주 공간을 확보하지 못하면 OutOfMemory Exception이 발생한다.
(참조)
Full GC를 수행할 때 죽은 오브젝트들을 메모리에서 밀어내는 작업을 흔히 Mark and Sweep이라고 부른다. 즉,살아있는 오브젝트들 "표시(Mark)"하고, Mark되지 않은 오브젝트들을 "쓸어내는(Sweep)" 작업이다.

아래 두 그림은 Sun JVM의 메모리 공간을 표현하고 있다. 그림과 위의 설명을 잘 조합하면 쉽게 이해할 수 있으리라 믿는다.





Sun JVM의 메모리 공간이 매우 다양한 요소들로 이루어져 있기 때문에, 이러한 개별 요소들이 다 세밀한 튜닝의 대상이 된다.

물론 다행인 것은 디폴트 값이나 알고리즘이 어느 정도의 성능을 보장하기 위해 많은 경우 메모리 크기 정도만을 지정하는 것으로 만족할 만한 성능을 얻을 수 있다는 것이다.

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이 정지된 시간 정보를 기록한다.

위의 옵션 들 중 가장 성능에 많은 영향을 주는 것은 Initail Heap Size, Max Heap Size, NewRatio, SurvivorRatio 그리고 GC의 종류 등이다. 해당 이 옵션 들의 정확한 의미를 꼭 이해하기 바란다. 몇가지 생각해 볼 점을 정리해보자...

New Generation의 크기가 미치는 영향은 어떨까?
New Generation의 크기가 크면 그만큼 Minor GC가 늦게 발생한다. 하지만 GC 시간은 길어진다. 지나치게 큰 New Generation의 문제는 Full GC를 유발할 수 있다는 것이다. 큰 크기의 Eden에 있던 오브젝트들이 한꺼번에 Tenured Space로 옮겨지는 상황에서 Full GC가 유발될 수 있다. New Generation의 크기가 지나치게 작으면 Minor GC가 지나치게 자주 발생하게 된다. 서버급 환경에서의 경험적 수치는 New:Old Generation의 크기를 1:2 ~ 1:8 정도에서 결정하는 것이다.

Survivor Ratio의 크기는 미치는 영향은 어떨까?
Eden에서 살아남은 오브젝트들은 Minor GC 시에 Survivor Space로 옮겨지고, Survivor Space가 꽉 차면 Tenured Space로 옮겨간다. 따라서 Survivor Ratio가 너무 작으면 Full GC가 자주 발생할 확률이 높아진다. 반면 이 값이 너무 크면 Eden Space가 작아서 Minor GC가 지나치게 자주 발생하게 된다. 서버급 환경에서의 경험적 수치는 Survivor Ratio의 값을 6 정도로 지정하는 것이다. 이렇게 되면 To:From:Eden = 1:1:6이 된다.

Max Heap Size가 주는 영향은 어떨까?
Max Heap Size가 주는 성능에 주는 영향은 절대적이다. 상식적으로 클 수록 좋다. Sun JVM의 Generation 관리 기법은 Heap Size가 크다고 해서 큰 성능의 저하가 없게끔 작동한다. 서버급 환경에서의 경험적 수치는 1G ~2G 사이의 값을 사용하고, Initial Size와 Max Size를 동일하게 줌으로써 동적 관리의 부담을 덜 수 있다.
하지만, 항상 큰 Heap이 좋은 것은 아니다. 불필요하게 큰 Heap Size는 Full GC에 지나치게 많은 시간이 걸리는 문제를 유발할 수 있다. Generation 기반 알고리즘의 기본 가정은 수명이 짧고 덩치가 작은 오브젝트는 많고, 수명이 길고 덩치가 큰 오브젝트 수는 적다는 것에 기반한다. 만일 이 가정이 어긋나면 예상치 못한 성능 문제가 생길 수 있다. 이런 경우에는 Heap Size를 줄이거나 New Generation의 크기 비율을 줄이는 방법을 사용할 수 있다. 혹은 Low Pause Collector를 사용하는 것이 바람직하다.(개인적으로는 Low Pause Collector를 사용해볼 것을 권장한다)

Max Heap Size 선정에서 한가지 주의할 점은 OS 레벨에서 Paging In/Out이 생기지 않아야 한다는 점이다. JVM을 구동할 당시에는 메모리 여유가 충분해서 문제가 없다가 JVM이 운영되는 중에 물리적 메모리가 부족하게 되면 Heap의 일부가 Page Out이 될 수 있다. 이런 상황에서는 JVM의 성능이 극도로 저하될 수 있다.

GC Dump
Garbage Collector의 종류에 따라 GC Dump의 포맷이 조금씩 다르다. GC Dump를 정확하게 이해하려면 이 차이를 이해할 필요가 있다.

(아래 예에서는 "-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime" 옵션을 이용해 GC Dump를 수행한다.)

일반적으로 GC 발생시 메모리 변동은 다음과 같은 포맷으로 기록이 된다.

"Generation종류: Before Used Size -> After Used Size (Total Size), 시간"

즉, "DefNew: 18175K->1983K(18176K), 0.0266109 secs"는 다음과 같은 사실을 의미한다.
  • New Generation에 대해 Serial GC를 수행했으며
  • GC 전에는 18M(18175K)를 쓰고 있었고, GC 이후 1.9M(1983K)를 쏘고 있다.
  • New Generation의 전체 크기는 18M(18176K) 이다.
  • GC를 수행하는데는 0.026초 정도 걸렸다.

아래에 간단한 포맷의 예제가 있으니 참조하기 바란다.
1. Serial Collector인 경우
0.272 [GC 0.272: [DefNew: 18175K->1983K(18176K), 0.0266109 secs] 52943K->41607K(260160K), 0.0266978 secs]
...

2. Throughput Collector인 경우
3.303: [GC [PSYoungGen: 15168K->3712K(15744K)] 251928K->244204K(257728K), 0.0386683 secs]
3.341: [Full GC [PSYoungGen: 3712K->0K(15744K)] [PSOldGen: 240492K->95306K(241984K)] 244204K->95306K(257728K) [PSPermGen: 1836K->1836K(16384K)], 0.4015217 secs]

Serial Collector에서 DefNew로 표기되었던 것이 PSYoungGen으로 표기된다. Throughtput Collector의 개념을 이해했다면 그 차이를 정확하게 알 수 있을 것이다.

3. Low Pause Collector인 경우
9.936: [GC [1 CMS-initial-mark: 173808K(253952K)] 174918K(262080K), 0.0011353 secs]
Total time for which application threads were stopped: 0.0029149 seconds
9.937: [CMS-concurrent-mark-start]
9.956: [GC 9.956: [ParNew: 8064K->0K(8128K), 0.0216583 secs] 181872K->176390K(262080K), 0.0217720 secs]
...
10.036: [GC 10.036: [ParNew: 8064K->0K(8128K), 0.0200405 secs] 187044K->181580K(262080K), 0.0201724 secs]
10.060: [CMS-concurrent-mark: 0.057/0.123 secs]
10.060: [CMS-concurrent-preclean-start]
10.061: [CMS-concurrent-preclean: 0.000/0.000 secs]
...
10.189: [GC[YG occupancy: 8064 K (8128 K)]10.190: [Rescan (parallel) , 0.0111151 secs]
10.201: [weak refs processing, 0.0000221 secs] [1 CMS-remark: 191976K(253952K)] 200040K(262080K), 0.0113344 secs]
...
10.663: [CMS-concurrent-sweep: 0.256/0.461 secs]
10.663: [CMS-concurrent-reset-start]
10.670: [CMS-concurrent-reset: 0.007/0.007 secs]
...
Serial Collector에서 DefNew로 표기되었던 것이 ParNew 로 표기되는 것에 주의한다. -XX:+UseParNewGC 옵션이 작동한 결과이다. CMS 로 시작하는 부분은 initial mark->concurrent mark -> remakr -> concurrent sweep 으로 이어지는 Full GC의 활동 상황이 기록된 것이다.

Garbage Collector간의 성능 테스트 예제
아래 표는 Serial Collector/Throughput Collector/Low Pause Collector와 NewRatio의 값을 바꾸어 가면서 성능 비교를 수행한 것이다. JVM 구동시 사용한 다른 옵션들은 다음과 같다
  • -server
  • -Xms=256M
  • -Xmx=256M
  • -XX:+PrintGCDetails
  • -XX:+PrintGCTimeStamps
  • -XX:+PrintGCApplicationStoppedTime
수행한 환경은 다음과 같다.
  • Sun JRE 1.5.06
  • Phsyical Memory 2G
  • CPU = 2 (따라서 JVM 기준으로 서버급 머신)
  • 수행한 프로그램은 첨부한 gc_dump.java 참조
아래에 수행 결과가 있다.

CollectorNewRatio=12
NewRatio=6NewRatio=2
Serial
수행 = 22574172
Pause Time = 14.3
Full GC:
Pause Time = 4.57,
Count = 11,
Average=0.416
Minor GC:
Pause Time = 9.68,
Count = 328, Average=0.029

수행 = 25665194
Pause Time = 14.03
Full GC:
Pause Time = 4.29,
Count = 13,
Average = 0.33
Minor GC:
Pause Time = 9.74,
Count = 195, Average=0.05
수행 = 21854332
Pause Time = 16.1
Full GC:
Pause Time = 11.46,
Count = 29,
Average = 0.39
Minor GC:
Pause Time = 4.68,
Count = 48, Average=0.097
Throughput
(Parallel)
수행 = 17699474
Pause Time = 16.3
Full GC:
Pause Time = 3.46,
Count = 8,
Average=0.43

Minor GC:
Pause Time = 12.8,
Count = 385, Average=0.033
수행 = 19805887
Pause Time = 16.1
Full GC:
Pause Time = 3.67,
Count = 9, Average=0.407

Minor GC:
Pause Time = 12.4,
Count = 233, Average=0.053
수행 = 18852852
Pause Time = 16.5
Full GC:
Pause Time = 5.98,
Count = 13,
Average=0.46

Minor GC:
Pause Time = 10.5,
Count = 103, Average=0.102
Low Pausse
(CMS)
수행 = 17791777
Pause Time = 12.8
Full GC:
Pause Time = 0.00,
Count = 11, Average=0.00

Minor GC:
Pause Time = 12.8,
Count = 555, Average=0.023
수행 = 19268025
Pause Time = 12.3
Full GC:
Pause Time = 0.00,
Count = 13, Average=0.00

Minor GC:
Pause Time = 12.3,
Count = 605, Average=0.02
수행 = 22121742
Pause Time = 13.8
Full GC:
Pause Time = 0.00,
Count = 8,
Average=0.00

Minor GC:
Pause Time = 13.8,
Count = 676, Average=0.02

위의 결과를 보면 Serial Collector이면서 NewRatio가 12인 경우가 Througput 면에서는 가장 우수한 성능을 발휘하는 것을 확인할 수 있다.
반면 Low Pause Collector인 경우에는 NewRatio가 2인 경우에 일량 기준으로도 우수하면서 Pause Time도 가장 낮은 것을 확인할 수 있다.

Low Pause Collector(CMS Collector)는 비록 Throughtput는 낮아질 수 있지만 사용자가 체감하는 응답 시간을 개선시킬 수 있다면 측면에서 적극적으로 활용해볼 만 하다.

Throughput Collector(Parallel Collector)는 가장 좋지 않은 성능을 나타내는데 그 이유는 테스트 장비가 실제로는 서버급이 아니기 때문이다. 많은 수의 CPU를 갖춘 서버급 장비에서는 최소한 Serial Collector보다는 좋은 성능을 내야하는 것이 일반적이다.


Heap Size에 따른 성능 테스트 예제
아래 표는 Heap Size를 256M ~ 1024M로 키우면서 성능을 측정한 결과이다.

Heap Size = 256M
(NewRatio = 2)
Heap Size = 512M
(NewRatio = 2)
Heap Size = 1024M
(NewRatio = 2)
Serial Collector
수행 = 21645002
Pause Time = 16.02
Full GC:
Pause Time = 11.68,
Count = 30,
Average = 0.39
Minor GC:
Pause Time = 4.34,
Count = 45,
Average = 0.096
수행 = 32468630
Pause Time = 13.56
Full GC:
Pause Time = 4.83,
Count 10,
Average = 0.48
Minor GC:
Pause Time = 8.56,
Count = 46,
Average = 0.19
수행 = 42292436
Pause Time = 11.38
Full GC:
Pause Time = 2.0,
Count = 3,
Average = 0.66
Minor GC:
Pause Time = 9.39
Count = 33,
Average = 0.28

위의 패턴을 보면 다음과 같은 사실을 확인할 수 있다.
  • Heap Size가 클 수록 일 처리량(Throughput)은 좋아진다. 그 이유는 GC가 덜 발생하기 때문이다.
  • Heap Size가 클 수록 GC는 적게 발생하지만, GC의 수행 시간은 더 길어진다.
위와 같은 속성을 잘 이해해야 Application에 최적화된 Heap 세팅이 가능하다.

---------------------------------------------------------------------------------
Heap과 GC가 워낙 중요한 이슈이기 때문에 글이 상당히 길어지고 말았다.

다음 글에서는 IBM JVM에 대해서...


신고
tags : , , ,
Trackback 0 : Comment 1
  1. 씨알 2008.05.18 00:05 신고 Modify/Delete Reply

    잘 읽고 갑니다. 좋은 정보 감사합니다.

Write a comment

티스토리 툴바