Garbage Collection 튜닝

dragonappear·2022년 7월 31일
0

JVM

목록 보기
4/6

출처

제목: "Garbage Collection 튜닝"
작성자: d2.naver.com(이상민)
작성자 수정일: 2012년 4월 6일 
링크: https://d2.naver.com/helloworld/37111
작성일: 2022년7월31일

GC 튜닝을 꼭 해야할까?

본격적으로 GC 튜닝을 살펴보기 전에 다음 질문에 대해 생각해보자.

모든 Java 기반의 서비스에서 GC 튜닝을 해야할까?

결론부터 이야기하면 모든 Java 기반의 서비스에서 GC 튜닝을 진행할 필요는 없다

GC 튜닝이 필요없다는 이야기는 운영 중인 Java 기반 시스템의 옵션과 동작이 다음과 같다는 의미이다.

  • -Xms 옵션과 -Xmx 옵션으로 메모리 크기를 지정했다.
  • -server 옵션이 포함되어있다.
  • 시스템에 Timeout 로그와 같은 로그가 남지 않는다.

다시 말하면, 메모리 크기도 지정하지 않고 Timeout 로그가 수도 없이 출력된다면 시스템에서 GC 튜닝을 하는 것이 좋다.

그런데 한 가지 꼭 명심해야 하는 점이 있다. GC 튜닝은 가장 마지막에 하는 작업이라는 것이다.

GC 튜닝을 하는 이유가 무엇인지 근본적인 원인을 생각해보자
Java에서 생성된 객체는 가비지 컬렉터가 처리해서 지운다.
생성된 객체가 많으면 많을수록 가비지 컬렉터가 처리해야 하는 대상도 많ㄴ아지고, GC를 수행하는 횟수도 증가한다.
즉, 운영하고 있는 시스템이 GC를 적게 하도록 하려면 객체 생성을 줄이는 작업을 먼저 해야 한다.

"티끌 모아 태산"이라는 말이 있듯이, String대신 StringBuilder나 StringBuffer를 사용하는 것을 생활화하는 것부터가 시작이라고 보면 된다.

그리고, 로그를 최대한 적게 쌓도록 하는 것이 좋다.

하지만 어쩔 수 없는 현실도 있다. 경험상 XML과 JSON 파싱은 메모리를 가장 많이 사용한다.

아무리 String을 최대한 사용 안 하고 Log 처리를 잘 하더라도, 10~100 MB짜리 XML이나 JSON를 파싱하면 엄청난 임시 메모리를 사용한다. 그렇다고 XML과 JSON을 사용하지 않기는 어렵다. 그냥 현실이 그렇다는 것만 알아주기 바란다.

만약 애플리케이션 메모리 사용도 튜닝을 많이 해서 어느 정도 만족할 만한 상황이 되었다면, 본격적으로 GC 튜닝을 시작하면 된다. 필자는 GC 튜닝의 목적을 두 가지로 나눈다.

Old 영역으로 넘어가는 객체의 수를 최소화하는 것과 Full GC의 실행 시간을 줄이는 것이다.


Old 영역으로 넘어가는 객체의 수 최소화하기

JDK 7부터 본격적으로 사용할 수 있는 G1 GC를 제외한, Oracle JVM에서 제공하는 모든 GC는 Generational GC이다.

즉, Eden 영역에서 객체가 처음 만들어지고, Survivor 영역을 오가다가, 끝까지 남아있는 객체는 Old 영역으로 이동한다.

간혹 Eden 영역에서 만들어지다가 크기가 커져서 Old 영역으로 바로 넘어가는 객체도 있긴 하다. Old 영역의 GC는 New 영역의 GC에 비하여 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 많이 줄일 수 있다.

Old 영역으로 넘어가는 객체의 수를 줄인다는 말을 잘못 이해하면 객체를 마음대로 New 영역에만 남길 수 있다고 생각할 수 있지만, 그렇게 할 수 는 없다. 하지만 New 영역의 크기를 잘 조절함으로써 큰 효과를 볼 수는 있다.


Full GC 시간 줄이기

Full GC의 실행 시간은 상대적으로 Minor GC에 비하여 길다. 그래서 Full GC 실행에 시간이 오래 소요되면(1초이상) 연계된 여러 부분에서 타임아웃이 발생할 수 있다. 그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 자칫 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어난다. 반대로 Old 영역의 크기를 늘리면 Full GC 횟수는 줄어들지만 실행 시간이 늘어난다.

Old 영역의 크기를 적절하게 잘 설정해야 한다.


GC의 성능을 결정하는 옵션

GC 옵션은 "누가 이 옵션을 썼을 때 성능이 잘 나왔대. 우리도 이렇게 적용하자."라고 생각하면 안된다. 왜냐하면, 서비스마다 생성되는 객체의 크기도 다르고 살아있는 기간도 다르기 때문이다.

아주 단순하게 생각해서, A, B, C, D, E라는 조건에서 어떤 작업이 수행되는 것과 A, B라는 조건에서 어떤 작업이 수행되는 것을 비교하면 어떤 조건에서 수행되는 작업이 더 빠를까? 일반적으로 그냥 생각해도 A, B 조건에서 수행되는 작업이 더 빠를 것이다.

Java의 GC 옵션도 마찬가지다. 이런 저런 옵션을 많이 설정한다고 시스템의 GC 수행 속도가 월등히 빨라지진 않는다. 오히려 더 느려질 확률이 높다.

두 대 이상의 서버에 GC 옵션을 다르게 적용해서 비교해 보고, 옵션을 추가한 서버의 성능이나 GC 시간이 개선된 때에만 옵션을 추가하는 것이 GC 튜닝의 기본 원칙다. 절대로 잊지 말자!

다음 표는 성능에 영향을 주는 GC 옵션 중 메모리 크기와 관련된 옵션이다.

이 중에서 필자가 GC 튜닝을 할 때 자주 사용하는 옵션은 -Xms 옵션, -Xmx 옵션, -XX:NewRatio 옵션이다. 특히 -Xms 옵션과 -Xmx 옵션은 필수로 지정해야 하는 옵션이다.
그리고 NewRatio 옵션을 어떻게 설정하느냐에 따라서 GC 성능에 많은 차이가 발생한다.

간혹 Perm 영역의 크기는 어떻게 설정해야 하는지 문의하는 분들이 있다. Perm 영역의 크기는 OutOfMemoryError가 발생하고, 그 문제의 원인이 Perm 영역의 크기 때문일 때에만 -XX:PermSize 옵션과 -XX:MaxPermSize 옵션으로 지정해도 큰 문제는 없다.

GC의 성능에 많은 영향을 주는 또 다른 옵션은 GC 방식이다. 다음 표는 GC 방식에 따라서 지정할 수 있는 옵션이다(JDK 6.0 기준).

G1 GC를 제외하고는, 각 GC 방식의 첫 번째 줄에 있는 옵션을 지정하면 GC 방식이 변경된다. GC 방식 중에서 특별히 신경쓸 필요가 없는 방식은 Serial GC다. Serial GC는 클라이언트 장비에 최적화되어 있기 때문이다.

이 외에도 GC의 성능에 영향을 주는 옵션은 많이 있다. 하지만 여기에 명시한 옵션만 제대로 지정하더라도 큰 효과를 볼 수 있다. 옵션이 많다고 GC 수행 시간이 좋아지는 것은 절대 아니다.


GC 튜닝의 절차

GC를 튜닝하는 절차도 대부분의 성능 개선 작업과 크게 다르지 않다. 다음은 필자가 사용하는 GC 튜닝 절차이다.

1. GC 상황 모니터링

GC 상황을 모니터링하며 현재 운영되는 시스템의 GC 상황을 확인해야 한다.

2. 모니터링 결과 분석 후 GC 튜닝 여부 결정

GC 상황을 확인한 후에는, 결과를 분석하고 GC 튜닝 여부를 결정해야 한다

분석한 결과를 확인했는데, GC 수행에 소요된 시간이 0.1~0.3초 밖에 안된다면 굳이 GC 튜닝에 시간을 낭비할 필요는 없다

하지만 GC 수행 시간이 1~3초, 심지어 10초가 넘는 상황이라면 GC 튜닝을 진행해야 한다.

그런데, 만약 Java의 메모리를 10GB 정도로 할당해서 사용하고 있고 메모리의 크기를 줄일 수 없다면 필자가 GC 튜닝에 대해서 안내해 줄 수 있는 방법이 없다. GC 튜닝 전에 시스템의 메모리를 왜 높게 잡아야 하는지에 대해 생각해 봐야만 한다. 만약 메모리르 1GB나 2GB로 지정했을때 OutOfMemoryError가 발생한다면, 힙 덤프를 떠서 그 원인을 확인하고, 문제점을 제거해야만 한다.

참고
힙 덤프는 현재 Java 메모리에 어던 객체와 어떤 데이터가 있는지 확인하기 위한 메모리의 단편 파일이라고 생각하면 된다. 이 파일은 JDK에 포함되어 있는 jmap 이라는 명령으로 생성할 수 있다. 파일을 생성하는 도중에는 java 프로세스가 멈추기 때문에 시스템을 운영하고 있을 때에는 이 파일을 생성하면 안된다.

3. GC 방식/메모리 크기 지정

GC 튜닝을 진행하기로 결정했다면 GC 방식을 선정하고 메모리의 크기를 지정한다. 이 떄 서버가 여러대이면 여러대의 서버에 GC 옵션을 서로 다르게 지정해서 GC 옵션에 따른 차이를 확인하는 것이 중요하다.

4. 결과 분석

GC 옵션을 지정하고 적어도 24시간 이상 데이터를 수집한 후에 분석을 실시한다. 운이 좋으면 해당 시스템에 가장 적합한 GC 옵션을 찾을 수 있다. 그렇지 않다멸 로그를 분석해 메모리가 어떻게 할당되는지 확인해야 한다. 그 다음에 GC 방식/메모리 크기를 변경해 가면서 최적의 옵션을 찾아 나간다.

5. 결과가 만족스러울 경우 전체 서버에 반영 및 종료

GC 튜닝 결과가 만족스러우면 전체 서버의 GC 옵션을 적용하고 마무리 한다.


GC 상황 모니터링 및 결과 분석하기

운영 중인 WAS(Web Application Server)의 GC 상황을 확인하는 가장 좋은 방법은 jstat 명령어를 사용하는 것이다.

다음 예제는 GC 튜닝을 안 한 어떤 JVM의 상황이다(참고로 운영 서버의 상황은 아니다).

이 중에서 YGC와 YGCT의 값을 확인한다. 두 값을 YGCT/YGC와 같이 나누면 0.050초(50ms)라는 값이 나온다. 즉, Young 영역에서 GC가 수행되는데 평균 50ms가 소요되었다는 말이다. 이 정도면 Young 영역의 GC는 신경쓰지 않아도 된다.

이번에는 FGCT와 FGC의 값을 확인한다. 두 값을 FGCT/FGC와 같이 나누면 19.68초라는 값이 나온다. 평균 19.68초가 소요되었다는 말이다. 세 번의 GC에서 모두 19.68초가 걸렸을 수도 있고, 두 번의 GC는 1초가 소요되고 한 번의 GC는 58초가 소요됐을 수도 있다. 그러나 어떤 경우이던 GC 튜닝이 필요한 경우라고 판단할 수 있다.

이렇게 GC의 상황을 jstat으로 간단하게 확인할 수도 있지만, –verbosegc 옵션으로 로그를 남겨 분석하는 것이 가장 좋다.

필자는 -verbosegc 로그를 분석하는 도구 중 HPJMeter를 가장 좋아한다.

사용법도 간단하고 분석하는 방법도 어렵지 않기 때문이다. HPJmeter를 사용하면, GC를 수행한 시간의 분포와 얼마나 자주 GC가 발생하는지를 쉽게 확인할 수 있다.

GC가 수행되는 시간을 확인했을 때 결과가 다음의 조건에 모두 부합한다면 GC 튜닝이 필요 없다.

  • Minor GC의 처리 시간이 빠르다(50ms내외).
  • Minor GC 주기가 빈번하지 않다(10초내외).
  • Full GC의 처리시간이 빠르다(보통1초이내).
  • Full GC 주기가 빈번하지 않다(10분에 1회).

위에서 괄호에 있는 값은 절댓값은 아니고 서비스의 상황에 따라 달라질 수 있는 값이다. Full GC 처리 속도가 0.9초가 나와도 만족하는 서비스가 있고, 그렇지 않은 서비스도 있기 때문이다. 따라서, 이와 같은 값을 확인하고 서비스의 특성에 따라 GC 튜닝 작업을 진행할지 결정한다.

한 가지 주의할 점은, GC 상황을 확인 할 때 Minor GC와 Full GC의 시간만 보면 안 된다는 점이다. GC가 수행되는 횟수도 확인해야 한다. 만약 New 영역의 크기가 너무 작게 잡혀 있다면 Minor GC가 발생하는 빈도도 매우 높을 뿐만 아니라(1초에 한번 이상인 경우도 있음), Old 영역으로 넘어가는 객체의 개수도 증가하게 되어 Full GC 횟수도 증가한다.

따라서 jstat명령의 –gccapacity 옵션을 적용하여 각 영역을 얼마나 점유하여 사용하는지도 확인해야 한다.


GC 방식/메모리 크기 지정

GC 방식 지정

GC 방식은 Oracle JVM을 기준으로 총 5가지가 있다. 그러나 JDK 7이 아니라면 Parallel GC, Parallel Compacting GC, CMS GC의 3개 중에 하나를 선택해야 한다.

이 중에서 어떤 방식을 선택해야 한다는 공식이나 원칙은 없다.

그렇다면, 어떻게 정해야 할까? 가장 좋은 방법은 3가지를 다 적용해 보는 것이다. 하지만, 한가지 확실한 것은 CMS GC가 다른 Parallel GC보다 빠르다는 것이다. 그렇다면 그냥 CMS GC만 적용하면 되겠지만, CMS GC가 항상 빠른 것은 아니다. 일반적인 CMS GC의 Full GC는 빠르지만, Concurrent mode failure가 발생하면 다른 Parallel GC보다 느리다.

Concurrent mode failure에 대해서 좀 더 알아 보자.

Parallel GC와 CMS GC의 가장 큰 차이점은 Compaction 작업 여부이다. Compaction 작업은 메모리 할당 공간 사이에 사용하지 않는 빈 공간이 없도록 옮겨서 메모리 단편화를 제거하는 작업이다.

Parallel GC 방식에서는 Full GC가 수행될 때마다 Compaction 작업을 진행하기 때문에 시간이 많이 소요된다. 하지만, Full GC가 수행된 이후에는 메모리를 연속적으로 지정할 수 있어 메모리를 더 빠르게 할당할 수 있다.

반대로 CMS GC는 Compaction 작업을 기본으로 수행하지 않는다. Compaction 작업을 수행하지 않기 때문에 당연히 속도가 빠르다. 하지만, Compaction 작업을 수행하지 않으면 디스크 조각 모음을 실행하기 전의 상태처럼 메모리에 빈 공간이 여기저기 생긴다. 그렇기 때문에 크기가 큰 객체가 들어갈 수 있는 공간이 없을 수 있다. 예를 들어, Old 영역에 남아 있는 크기가 300MB인데도 10MB짜리 객체가 연속적으로 들어갈 공간이 없을 수도 있다는 말이다. 그럴 때 Concurrent mode failure라는 경고가 발생하면서 Compaction 작업을 수행한다. 그런데, CMS GC를 사용할 때에는 Compaction 시간이 다른 Parallel GC보다 더 오래 소요된다. 그래서 오히려 더 문제가 될 수 있다.

결론적으로, 운영 중인 시스템에 가장 적합한 GC 방식을 찾아 내야 한다.

운영 중인 시스템 특성에 따라 적합한 GC 방식이 다르므로 해당 시스템에 가장 적합한 방식을 찾아야 한다. 운영 중인 서버가 6대 정도 있다면, 2대씩 각 옵션을 동일하게 지정하고 -verbosegc 옵션을 추가한 후 결과를 분석하는 방법을 추천한다.

메모리 크기 지정

메모리 크기와 GC 발생 횟수, GC 수행 시간의 관계는 다음과 같다.

메모리크기가크면

  • GC 발생횟수는줄어든다.
  • GC 수행시간은길어진다.

메모리크기가작으면

  • GC 수행시간은적어진다.
  • GC 발생횟수는증가한다.

메모리 크기를 크게 설정할 것인지, 작게 설정할 것인지에 대한 정답 역시 없다. 서버 자원이 좋은 시스템이라 메모리를 10GB로 설정해도 Full GC가 1초 이내에 끝난다면 10 GB로 지정해도 된다. 하지만, 대부분의 서버는 그렇지 못하다. 메모리를 10GB 정도로 설정하면 Full GC 시간이 10~30초 정도 소요된다. 물론 이 시간은 객체의 크기가 어떻게 되어 있느냐에 따라서 달라진다.

그렇다면 메모리 크기를 얼떻게 설정해야 할까?

필자는 보통 500MB로 설정하라고 이야기한다. 그렇다고 WAS의 메모리를 –Xms500m 옵션과 –Xmx500m 옵션으로 지정하라는 이야기는 절대 아니다.

GC 튜닝 이전에 현재 상황을 모니터링한 결과를 바탕으로 Full GC가 발생한 이후에 남아 있는 메모리의 크기를 봐야 한다.

만약 Full GC 후에 남아 있는 메모리가 300MB 정도라면 300MB(기본 사용) + 500MB(Old 영역용 최소) + 200 MB(여유 메모리)를 감안하여 1GB 정도로 지정하는 것이 좋다.

즉, Old 영역을 위해서 500MB 이상 여유가 있는 공간을 지정해야 한다는 말이다. 그래서 3대 정도의 운영 서버가 있다면, 서버 한대는 1GB로, 다른 한대는 1.5 GB로, 또 다른 한대는 2GB 정도로 지정한 후 결과를 지켜 본 다음 결정한다.

이렇게 지정하면, 이론적으로 생각 했을 때에는 당연히 1GB > 1.5GB > 2GB 순서로 GC가 빠르고, 결국 1GB일 때 GC가 제일 빠를 것이다.

하지만 그렇다고 1GB일 때 Full GC가 1초 걸리고, 2GB일 때 2초 걸린다고 보장할 수 없다. 서버의 성능에 따라 다르고 객체의 크기에 따라서 시간이 달라지기 때문이다. 그러므로 측정 데이터 셋을 최대한 많이 만들어 모니터링을 통해서 확인하는 것이 가장 좋은 방법이다.

메모리 크기를 지정할 때 지정해야 하는 것이 한 가지 더 있다. 바로 NewRatio다. NewRatio는 New 영역과 Old 영역의 비율이다. –XX:NewRatio=1로 지정하면 New 영역:Old 영역이 1:1이 된다. 만약 1GB라면 New 영역:Old 영역은 500MB:500MB가 된다. NewRatio가 2이면 New 영역:Old 영역이 1:2가 된다.

즉, 값이 커지면 커질수록 Old 영역의 크기가 커지고 New 영역의 크기가 작아진다.

별 것이 아닌 것처럼 생각할 수 있지만, NewRatio 값은 GC의 전반적인 성능에 많은 영향을 준다.

New 영역의 크기가 작으면 Old 영역으로 넘어가는 메모리의 양이 많아져서 Full GC도 잦아지고 시간도 오래 걸린다.

단순하게 생각해서 NewRatio 값을 1로 주면 최고의 상황이 될 수 있다고 볼 수도 있지만, 꼭 그렇지만은 않다. 오히려 NewRatio의 값이 2나 3일 때의 전반적인 GC 상황이 좋을 수 있다. 필자의 경험으로도 그렇다.

GC 튜닝을 가장 빨리 진행하는 방법은 무엇일까?

성능 테스트로 결과를 비교하는 것이 가장 빠른 검토 결과를 얻을 수 있는 방법이다.

운영 서버마다 옵션을 다르게 지정하고 상황을 모니터링하려면, 적어도 하루에서 이틀 정도 데이터가 쌓인 후에 보는 것이 바람직하다.

하지만 성능 테스트를 통해서 GC 튜닝을 할 때는 운영 상황과 동일하게 부하를 주도록 준비하는 과정이 필요하다. 그리고 부하를 주는 URL과 같은 요청 비율도 운영과 동일해야 한다. 그러나 이렇게 정확하게 부하를 주는 것은 전문 성능 테스터도 쉽지 않고, 준비하는 데 오히려 더 많은 시간이 소요될 수 있다. 시간이 오래 걸리더라도 운영에 적용하고 기다리는 것이 더 간단하고 편하다.


GC튜닝 결과 분석

GC 옵션을 적용하고, -verbosegc 옵션을 지정한 다음에 tail 명령어로 로그가 제대로 쌓이고 있는지 확인해야 한다.

만약 옵션을 잘못 지정해서 로그가 안 쌓이면, 시간만 허비하기 때문이다. 로그가 잘 쌓이고 있다면, 하루 혹은 이틀 정도의 데이터가 축적된 후 결과를 확인해 보자. 로그를 로컬 PC로 옮긴 다음에 HPJMeter로 분석하는 것이 가장 쉽다.

분석할 때에는 다음의 사항을 중심으로 살펴보는 것이 좋다.

순서는 필자 나름의 기준에 따른 우선 순위다.

GC 옵션을 결정하는 데 가장 큰 비중을 차지하는 것은 1번 항목인 Full GC 수행 시간이다.

  • Full GC 수행시간
  • Minor GC 수행시간
  • Full GC 수행간격
  • Minor GC 수행간격
  • 전체 Full GC 수행시간
  • 전체 Minor GC 수행시간
  • 전체 GC 수행시간
  • Full GC 수행횟수
  • Minor GC 수행횟수

운이 좋아서 한 번에 가장 적합한 GC 옵션을 찾으면 좋지만, 그렇지 못한 경우가 대부분이다. 한 번에 끝내려다가 잘못하면 서비스에 OutOfMemoryError가 발생할 수 있으니 조심해서 GC 튜닝을 진행하는 것이 좋다.


튜닝 사례

사례1

아래 예는 S 서비스의 GC 튜닝 사례이다. 신규로 개발된 S 서비스는 Full GC를 수행하는데 시간이 오래 소요되고 있었다.

먼저 jstat –gcutil의 결과를 보자.

왼쪽에 있는 Perm 영역까지의 정보는 처음 GC 튜닝을 할 때에는 중요하지 않다. 오른쪽에 있는 YGC부터의 값이 중요하다.

Minor GC와 Full GC가 한 번 수행될 때 평균 얼마나 소요되었는지 계산하면 다음과 같다.

Minor GC를 수행하는데 37ms면 양호한 상황이다.
하지만, Full GC가 평균 1.389초 걸렸다는 것은 DB Timeout을 1초로 한 시스템에서는 GC가 발생할 때 많은 Timeout이 발생할 수 있다는 말이 된다. 이런 상황의 시스템은 GC 튜닝을 해야 한다.

이 상태에서 무작정 GC 튜닝을 시작하면 안 되고 메모리를 어떻게 사용하고 있는지 살펴봐야 한다.
메모리 사용량은 jstat –gccapacity 옵션으로 확인한다.
이 서버에서 확인한 결과는 다음과 같다.

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC  
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5  

주요한 값만 살펴 보면 다음과 같다.

  • New 영역사용크기: 212,992 KB
  • Old 영역사용크기: 1,884,160 KB

즉, 전체 할당된 메모리의 크기는 Perm 영역을 제외하고 2GB이며, New 영역:Old 영역이 1:9이다.
jstat보다 더 상세하게 상황을 확인하기 위해서 -verbosegc 로그를 추가하고, 3개의 인스턴스에 다음과 같이 3가지 옵션을 지정했다. 다른 옵션은 별도로 추가하지 않았다.

  • NewRatio=2
  • NewRatio=3
  • NewRatio=4

하루 정도 지난 이후 이 시스템의 GC 로그를 확인했다. 운이 좋게도 이 시스템은 NewRatio를 지정한 이후에 Full GC가 한번도 발생하지 않았다.

왜 그랬을까? 그 이유는 해당 시스템에서 생성되는 객체는 대부분 금방 소멸되는 객체이며, 객체가 생성된 이후에 Old 영역으로 넘어가지 않고, New 영역에서 모두 사라져 버리기 때문이다.

이 상황에서는 다른 옵션은 변경할 필요도 없다. 따라서, NewRatio 값 중에서 가장 좋은 값을 선택하면 된다. 가장 좋은 값은 어떻게 판단할까? 각 NewRatio의 Minor GC 평균 응답 시간을 분석하면 된다.

각 옵션별 평균 응답시간은 다음과 같다.

  • NewRatio=2 : 45 ms
  • NewRatio=3 : 34 ms
  • NewRatio=4 : 30 ms

New 영역의 크기는 가장 작지만 GC시간이 짧은 NewRatio=4이 가장 좋은 옵션이라는 결론을 내렸으며, GC 옵션을 적용한 이후에 이 서버에서는 Full GC가 발생하지 않았다.

참고로 해당 서비스의 JVM이 시작하고 며칠이 지난 후에 수행한 jstat –gcutil 결과는 다음과 같다.

서버에 요청이 많지 않아서 GC가 자주 발생하지 않았다고 생각할 수도 있다. 하지만, Minor GC가 2,424 번 수행될 동안 Full GC는 한 번도 수행되지 않았다.

튜닝 사례2

이번 사례는 A 서비스의 사례이다. A 서비스에 GC 튜닝을 진행하게된 이유는 사내에 운영 중인 APM(Application Performance Manager)에서 주기적으로 JVM이 오랫동안(8초 이상) 동작하지 않는다는 것을 발견했기 때문이다. 원인을 찾던 중 Full GC 시간이 오래 소요되는 것을 보고 GC 튜닝을 진행하기로 했다.

튜닝의 첫 단계로 -verbosegc 옵션을 추가했으며, 결과는 다음과 같았다.

위의 그래프는 HPJMeter가 분석 후 자동으로 제공하는 그래프 중 Duration 그래프이다.
JVM이 시작되었을 때부터의 시간이 X축이며, 각 GC의 응답 시간이 Y축이다. CMS로 표시된 것은 Full GC이며, Parallel Scavenge로 표시된 것은 Minor GC 결과다.

CMS GC가 제일 빠르다고 했는데, 결과를 보면 15초까지 소요된 것도 있다. 왜 이러한 결과가 나왔을까?
앞에서 CMS가 Compaction 단계를 거치면 오히려 더 느려진다고 설명한 것을 기억한다면 그 이유를 이해할 수 있을 것이다. 게다가 해당 서비스는 메모리를 –Xms1g –Xmx4g로 지정해 놓았고, 4GB까지 메모리를 할당해서 사용하고 있었다.

일단 CMS GC 대신 Parallel GC로 변경했다. 메모리 크기는 2GB로 변경하고, NewRatio를 3으로 지정했다. 이렇게 지정하고 몇 시간 후의 jstat –gcutil 결과는 다음과 같다.

4GB일 때 15초에 비하면 Full GC 시간이 회당 3초 정도로 빨라지긴 했지만, 3초도 빠른 것은 아니다. 그래서 다음과 같이 6개의 케이스를 만들어 변경해 보았다.

  • Case1 : -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • Case2 : -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • Case3 : -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3
  • Case4 : -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • Case5 : -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • Case6 : -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

이 중 어떤 것이 가장 빠르게 나왔을까? 당연히 할당된 메모리의 크기가 작을 수록 유리한 결과가 나왔다. 가장 GC개선율이 높은 Case6의 Duration 그래프는 다음과 같다. 가장 느린 응답 속도가 1.7초 정도이며, 평균 1초 이내의 양호한 상황으로 바뀌었다.

이 결과를 보고 해당 서비스의 GC 옵션을 모두 Case 6으로 변경했다. 하지만, 이것이 원인이 되어 그날 저녁에 OutOfMemoryError가 발생하는 문제가 있었다. 여기서 그 원인을 자세하게 이야기하기는 어려우나, 대량으로 데이터를 처리하는 배치 작업때문에 JVM의 메모리가 부족해졌다. 관련된 문제를 해결하는 작업은 지금 진행 중이다.

GC 튜닝을 진행할 때에는 단순히 짧은 시간 동안 쌓인 GC 로그만 분석하고 전체 서버에 적용하는 것은 매우 위험하다. 서비스가 어떻게 운영되고 있는지에 대한 분석도 같이 진행되어야 장애 없이 GC 튜닝이 가능하다는 것을 잊지 말기 바란다.

지금까지 간단하게 두 건의 GC 튜닝 사례로 GC튜닝을 어떻게 진행하는지 살펴 보았다. 다시 이야기하지만, 필자가 튜닝 사례에서 지정한 GC 옵션은 동일한 기능을 수행하는 서비스의 동일한 CPU 및 운영체제 버전, 동일한 JDK 버전을 갖는 서버에는 일괄적으로 지정해도 된다. 하지만, 여러분이 지금 운영 중인 서비스에 이 옵션을 그대로 적용해서는 절대 안 된다.

0개의 댓글