태터데스크 관리자

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

태터데스크 메시지

저장하였습니다.

[Enterprise Java는 거대한 동기화 머신이다] - 재미있는 GC 성능 사례 - Part1

Enterprise Java 2007.09.10 22:09
Java에서의 Garbage Collection을 흔히 필요악으로 묘사한다. 확실히 GC는 편리한 객체 사용이라는 장점을 제공한다. 하지만 Java에서 발생하는 성능 저하 현상의 8,90%는 GC와 직간접적으로 관련이 있다고 해도 무방할 정도로 문제를 일으키기도 한다.

Garbage Collection은 [맹견]과 같다. 잘 길들이면 행복을 주지만, 자칫 치명적인 상처를 입힐 수도 있다.

GC에서 문제가 발생하는 상황은 매우 다양하다. 더구나 각 시스템마다 Unique한 성능 특징을 가지고 있어 일반적인 해결책이 무용한 경우가 많다.

하지만, 보편적으로 발생하는 몇 가지 사례를 통해 의미있는 통찰력을 얻을 수 있다는 사실은 확실하다. 이런 의미에서 앞으로 몇 차례에 걸쳐 전형적이면서 재밌는 GC 사례와 해결책을 논의해보고자 한다.


1. RMI 사용에 의한 주기적인 Full GC 발생

RMI를 사용할 경우 주기적으로(정확하게 말하면 60초마다) System.gc()를 호출한다.

아래의 GC Dump 예를 보자.

...
211.207: [Full GC 211.207: [ParNew: 123251K->24523K(742784K), 0.2391338 secs] ...
278.167: [Full GC 278.167: [ParNew: 140194K->3468K(742784K), 0.9600674 secs] ...
342.531: [Full GC 342.531: [ParNew: 81241K->646K(742784K), 0.5440857 secs] ...
...

위의 예를 보면 278 - 211 = 67(초), 342 - 278 = 64(초)로 항상 60여 초 단위로 Full GC가 발생하고 있다. 이런 현상이 목격되면 RMI에서 사용하는 Distributed GC에 의한 Explicit GC일 가능성이 대부분이다. RMI 통신을 위해 생성된 객체들을 주기적으로 정리하기 위해 Distributed GC가 작동하게 된다.

아래 2개의 옵션이 Distributed GC에 의한 Explicit GC의 간격을 결정한다.

sun.rmi.dgc.server.gcInterval
sun.rmi.dgc.client.gcInterval

JDK 1.6 이전에는 이 옵션들의 기본값이 60000(ms), 즉 60초였다. 60초마다 한번씩 System.gc()를 호출하게 된다.

System.gc() 호출에 의한 Explicit GC 작업은 Collector의 종류와 무관하게 항상 STW(Stop-The-World) 방식의 Full GC를 유발한다. 심지어 Low Pause Collector(-XX:+UseConcMarkSweepGC)를 사용하더라도 System.gc()는 여전히 STW 방식으로 작동한다. 큰 크기의 Heap을 사용하는 Application에서는 이런 빈번한 Full GC의 호출이 큰 성능 문제를 일으킨다.

기본적인 가이드는 다음과 같다.

1. -XX:+DisableExplicitGC 옵션을 사용해서 System.gc() 콜을 무의미하게 만들어 버린다. 가장 극단적이고 확실한 해결책이다.
2. -Dsun.rmi.dgc.server.gcInterval=3600000 -Dsun.rmi.dgc.client.gcInterval=3600000 (한시간) 과 같이 DGC Interval을 크게 준다.

JDK 1.6 부터는 Low Pause Collector(Concurrent Collector, -XX:+UseConcMarkSweepGC)를 사용할 경우 다음 옵션을 사용할 수 있다.
3. -XX:+ExplicitGCInvokesConcurrent 옵션을 사용해서 System.gc() 콜이 STW(Stop-The-World) 방식이 아닌 Concurrent 방식으로 이루어지도록 한다.


Distributed GC를 논외로 하더라도 Explicit GC는 절대로 남용되어서는 안된다. 메모리를 많이 사용하는 Application들에서 메모리를 좀 더 효율적으로 사용하기 위해 System.gc()를 틈틈이 호출하는 코드를 삽입하는 오류를 흔히 보게 되는데, 굉장히 잘못되고 위험한 방식이다.

Java의 자동화된 GC는 Explicit GC의 호출을 거의 필요로 하지 않는다. 따라서 무의미한 System.gc() 호출이 발생하지 않도록 해야 한다.

PS) 그렇다면, Explicit GC는 완전히 무용한 것일까? 흐음...
생각해 볼 문제다. 다음과 같은 경우에는 유용할 수도 있다.
-XX:+UseConcMarkSweepGC에 의해 활성화되는 Low Pause Collector는 Full GC를 Concurrent하게 진행한다. 이것을 가능하게 하기 위해 Compaction(압축) 작업을 하지 않는다. 따라서 Low Pause Collector에서는 Fragmentation이 발생할 확률이 높다.
Fragmentation이 지나쳐서 필요한 메모리를 확보하지 못하면 STW 방식의 Full GC가 발동되고, 비로소 Compaction 작업이 이루어진다. 웁스!!! 만일 이런 일이 시스템에 로드가 많이 걸린 시점에 발생했다면? Concurrent Collector의 장점이 사라져 버리는 것이다.

Low Pause Collector를 사용하는 환경에서 Fragmentation에 의한 STW Full GC가 자주 발생한다고 판명되면, 시스템의 로드가 적은 시점에 주기적으로 System.gc()를 호출하는 것도 한가지 솔루션이 될 수 있다. 아주 특수한 경우이지만 JVM 옵션으로도 해결이 안되는 경우에는 Workaround가 될 수 있다는 의미이다. 물론 정교한 JVM 옵션 설정으로 이런 문제가 생기지 않도록 하는 것이 최선이겠지만...




신고
Trackback 0 : Comments 3
  1. 욱짜 2007.09.11 17:25 신고 Modify/Delete Reply

    (참고) JMX를 사용하는 경우 RMI에 의한 Full GC가 자연스럽게 생기니 특히 주의가 필요합니다. 다행히 JDK 1.6에서는 DGC Interval이 1시간으로 늘었지만 이전 버전이라면 반드시 확인 요망!!

  2. 아쭈 2007.11.12 21:37 신고 Modify/Delete Reply

    퍼가겠습니다^^좋은 글 감사합니다^^

  3. 쫑맹 2012.03.29 11:10 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment

티스토리 툴바