본문 바로가기

Java/자바 성능 개선

19. GC 튜닝을 항상 할 필요는 없다.

💡 본 게시글은 이상민 저자의 '자바 성능 튜닝 이야기' 교재를 공부하고, 이에 대해 정리한 내용입니다.

들어가며

 자바의 GC 튜닝은 반드시 필요한 상황에서만 진행하는 것이 바람직합니다. 그렇다고 WAS를 실행할 때 아무런 옵션 없이 실행해도 되는 것은 아닙니다. 기본적인 메모리 크기를 지정하는 등의 설정은 필요합니다. 그러나 사용량이 많지 않은 시스템에서는 이 정도의 설정으로 충분하며, 추가적인 튜닝은 굳이 필요하지 않을 수 있습니다.


1) GC 튜닝을 꼭 해야 할까?

 결론부터 말하자면, Java 기반의 모든 서비스에서 GC 튜닝을 해야 하는 것은 아닙니다. 다만, 아래와 같은 설정들이 기본적으로 이루어져 있어야 합니다:

  • 메모리 크기를 -Xms 옵션과 -Xmx 옵션으로 지정하였다. 
  • -server 옵션이 포함되어 있다. 
  • 시스템 로그에는 DB 작업이나 다른 서버와의 통신에 관련된 타임아웃 로그가 없어야 한다.

 

 타임아웃 로그가 있다면, 그 시스템을 사용하는 사용자들 중 일부나 대다수가 정상적인 응답을 받지 못했다는 의미입니다. 이는 서버 간의 통신 문제나 원격 서버의 성능 저하로 인해 발생할 수 있지만, GC 때문일 가능성도 있습니다.

 

따라서, JVM의 메모리 크기를 지정하지 않았거나, 타임아웃이 지속적으로 발생하고 있다면 GC 튜닝을 고려해야 합니다. 그렇지 않다면, GC 튜닝을 하는 시간에 다른 작업을 하는 것이 더 효율적일 수 있습니다. 그러나 중요한 것은, GC 튜닝은 가장 마지막에 고려해야 하는 작업이라는 점입니다.

 

가비지 컬렉터가 처리해야 하는 대상이 많아질수록, 즉 객체가 많이 생성될수록 GC를 수행하는 횟수도 증가합니다. 따라서, GC를 적게 하려면 먼저 객체 생성을 줄이는 작업이 필요합니다. 예를 들어, String 대신 StringBuilder나 StringBuffer를 사용하거나, 로그를 최대한 적게 쌓는 등의 방법으로 임시 메모리 사용을 줄여야 합니다.

 

이러한 애플리케이션 메모리 사용 튜닝을 충분히 진행한 후에 만족할 만한 결과를 얻었다면, 이후에 GC 튜닝을 진행하면 됩니다. GC 튜닝은 크게 두 가지 목표를 가지고 있습니다: Old 영역으로 넘어가는 객체의 수를 최소화하고, Full GC의 실행 시간을 줄이는 것입니다.

 

Old 영역으로 넘어가는 객체의 수를 줄이려면, New 영역의 크기를 잘 조절하는 것이 중요합니다. Old 영역의 GC는 New 영역의 GC보다 시간이 많이 소요되므로, Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 줄일 수 있습니다. 그리고 Full GC의 수행 시간을 줄이려면, Old 영역의 크기를 적절히 조절해야 합니다. Old 영역의 크기를 줄이면 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어날 수 있습니다. 반대로 Old 영역의 크기를 늘리면 Full GC 수행 횟수는 줄어들지만 실행 시간이 늘어날 수 있습니다.


2) GC 성능을 좌우하는 옵션

 GC 옵션 설정은 '어떤 서비스에서 성능이 좋았다'는 이유만으로 선택해서는 안 됩니다. 서비스마다 객체의 생성 크기와 생존 기간이 다르기 때문입니다.

 

GC 튜닝의 기본 원칙은 두 대 이상의 서버에 각기 다른 GC 옵션을 적용해 비교하고, 옵션을 추가한 서버의 성능이나 GC 시간이 개선될 때에만 해당 옵션을 채택하는 것입니다.

 

아래 표는 성능에 영향을 주는 GC 옵션 중 메모리 크기와 관련된 것들입니다.

구분 옵션 설명
힙(heap) 영역 크기 -Xms JVM 시작 시 힙 영역 크기
힙(heap) 영역 크기 -Xmx 최대 힙 영역 크기
New 영역의 크기 -XX:NewRatio New 영역과 Old 영역의 비율
New 영역의 크기 -XX:NewSize New 영역의 크기
New 영역의 크기 -XX:SurvivorRatio Eden 영역과 Survivor 영역의 비율

 

이 중에서 가장 자주 사용되는 옵션은 -Xms, -Xmx, -XX:NewRatio 입니다. 특히 -Xms와 -Xmx는 필수로 지정해야 하는 옵션이며, NewRatio 옵션 설정에 따라 GC 성능에 큰 차이를 보일 수 있습니다.

 

 Perm 영역의 크기는 OutOfMemoryError가 발생하고, 그 원인이 Perm 영역의 크기 때문일 때에만 -XX:PermSize 옵션과 -XX:MaxPermSize 옵션으로 설정해도 됩니다.

 

GC 성능에 큰 영향을 주는 다른 옵션은 GC 방식입니다. 아래 표는 GC 방식에 따라 지정할 수 있는 옵션을 보여줍니다(JDK 6.0 기준).

구분 옵션 비고
Serial GC -XX:+UseSerialGC  
ParallelGC -XX:+UseParallelGC
-XX:ParallelGCThreads=value
 
Parallel Compacting GC -XX:+UseParallelOldGC  
CMS GC -XX:+UseConcMarkSweepGC
-XX:+UseParlNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly
 
G1 -XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC
JDK 6에서는 두 옵션을 반드시 같이
사용해야 함

 

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


3) GC 튜닝 절차

GC 튜닝은 성능 개선 작업과 크게 다르지 않은 절차를 따릅니다.

1단계: GC 상황 모니터링 현재 운영 중인 시스템의 GC 상황을 확인합니다.

 

2단계: 모니터링 결과 분석 및 GC 튜닝 결정 GC 상황을 확인한 후 결과를 분석하고, GC 튜닝이 필요한지 결정합니다. 만약 GC 튜닝에 소요된 시간이 0.1초 ~ 0.3초라면 GC 튜닝에 시간을 낭비할 필요가 없습니다. 하지만, GC 수행 시간이 1~3초, 심지어 10초가 넘는다면 GC 튜닝을 진행해야 합니다.

 

3단계: GC 방식 및 메모리 크기 설정 GC 튜닝을 진행하기로 결정했다면, GC 방식을 선택하고 메모리 크기를 설정합니다. 여러 대의 서버가 있다면, 각 서버에 서로 다른 GC 옵션을 지정하여 GC 옵션에 따른 차이를 확인하는 것이 중요합니다.

 

4단계: 결과 분석 GC 옵션을 설정하고 24시간 이상 데이터를 수집한 후에 분석을 진행합니다. 이 과정에서 메모리가 어떻게 할당되는지 확인하고, GC 방식과 메모리 크기를 변경하면서 최적의 옵션을 찾아냅니다.

 

5단계: 결과가 만족스럽다면 전체 서버에 반영하고 종료 GC 튜닝 결과가 만족스러우면, 전체 서버에 GC 옵션을 적용하고 작업을 마무리합니다. 이제 각 단계에서 해야하는 작업에 대해 좀 더 자세히 알아보겠습니다.

 

1, 2 단계: GC 상황 모니터링 및 결과 분석

 운영 중인 WAS의 GC 상황을 확인하는 가장 좋은 방법은 'jstat' 명령어를 사용하는 것입니다. 이 명령어를 사용해 어떤 데이터를 확인해야 하는지 알아봅시다.

 

예를 들어, GC 튜닝을 하지 않은 JVM의 상황을 확인해봅시다. 이때 'jstat -gcutil' 명령어를 사용하여 YGC, YGCT, FGC, FGCT 값을 확인하고, 이를 통해 GC가 수행되는 시간을 파악합니다. Minor GC와 Full GC의 처리 시간과 주기를 확인하며, 이들이 빠르고 빈번하지 않다면 GC 튜닝이 필요 없습니다.

 

그러나, 단지 Minor GC와 Full GC의 시간만 보는 것은 부족합니다. GC가 수행되는 횟수도 확인해야 합니다. 만약 New 영역의 크기가 너무 작다면 Minor GC의 빈도가 매우 높을 뿐만 아니라, Old 영역으로 넘어가는 객체의 개수도 증가하게 되어 Full GC 횟수도 증가합니다. 이런 사항도 고려하여 'jstat -gccapacity' 명령을 사용해 각 영역이 얼마나 점유되고 있는지도 확인해야 합니다.

 

3-1 단계 : GC 방식 지정

 GC 방식 지정 아제 튜닝을 위해 GC 방식과 메모리 크기를 지정하는 방법을 다루겠습니다.

우선, GC 방식은 Oracle JVM 기준으로 총 5가지가 있지만, Serial GC는 운영에서 사용할 수 없습니다. 또한, JDK 7이 아닌 경우 G1을 제외해야하므로, Parallel GC, Parallel Compacting GC, CMS GC 중 하나를 선택해야 합니다.

 

 GC 방식을 결정하는 가장 좋은 방법은 3가지를 모두 적용해보는 것입니다.

 

일반적으로 CMS GC는 Parallel GC보다 작업 속도가 빠르지만, 항상 그런 것은 아닙니다. CMS GC의 Full GC 처리 시간은 빠르지만, Concurrent mode failure가 발생하면 다른 Parallel GC보다 느려집니다.

 

Parallel GC와 CMS GC의 가장 큰 차이점은 압축(compaction) 작업 여부입니다. 압축 작업은 메모리 할당 공간 사이에 사용하지 않는 빈 공간을 없애서 메모리 단편화를 제거하는 작업입니다.

 

 Parallel GC에서는 Full GC가 수행될 때마다 압축 작업을 진행하므로 시간이 많이 소요됩니다. 하지만, Full GC가 수행된 후에는 메모리를 더 빠르게 할당할 수 있습니다.

 

반면, CMS GC는 기본적으로 압축 작업을 수행하지 않아 속도가 빠릅니다. 하지만, 압축 작업을 수행하지 않으면 메모리에 빈 공간이 생기며, 크기가 큰 객체가 들어갈 수 있는 공간이 없을 수 있습니다. 이 때 Concurrent mode failure 경고가 발생하며 압축 작업을 수행합니다. CMS GC를 사용할 때 압축 시간이 다른 Parallel GC보다 더 오래 걸리므로, 오히려 문제가 될 수 있습니다.

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

 

3-2 단계 : 메모리 크기

이 부분에서는 JVM의 시작 크기(-Xms)와 최대 크기(-Xmx)에 대해 다룹니다. 메모리 크기와 GC 발생 횟수, GC 수행 시간의 관계는 다음과 같습니다.

 

메모리 크기가 크면,

GC 발생 횟수는 감소하지만, GC 수행 시간은 길어집니다.

 

반대로, 메모리 크기가 작으면,

GC 발생 횟수는 짧아지고, GC 수행 시간은 증가합니다.

 

메모리 크기를 어떻게 설정해야 할 지에 대한 정답은 없습니다. 서버 자원이 좋은 시스템에서는 메모리를 10GB로 설정해도 Full GC가 1초 이내에 끝나지만, 대부분의 서버에서는 메모리를 10GB로 설정하면 Full GC 시간이 10-30초 정도 소요됩니다. 이 시간은 객체의 크기에 따라 달라집니다. 메모리 크기를 결정하기 위해, GC 튜닝 이전에 현재 상황을 모니터링한 결과를 바탕으로 Full GC가 발생한 이후에 남아 있는 메모리의 크기를 확인해야 합니다.

 

만약 Full GC 후에 남아 있는 Old 영역의 메모리가 300MB라면, 300MB(기본 사용) + 500MB(Old 영역용 최소) + 200MB(여유 메모리)를 감안하여 Old 영역만 1GB로 지정하는 것이 좋습니다. 이렇게 지정하면, 이론적으로 생각했을 때 GC 수행 시간은 Old 영역 1GB > 1.5GB > 2GB 순서로 빠르므로, 1GB일 때 GC가 가장 빠르다고 볼 수 있습니다.

 

하지만, 서버의 성능과 객체의 크기에 따라 시간이 달라질 수 있으므로, 측정 데이터를 많이 만들어서 모니터링을 통해 확인하는 것이 가장 좋습니다. 메모리 크기를 지정할 때 고려해야 하는 또 다른 요소는 NewRatio입니다.

 

NewRatio는 New 영역과 Old 영역의 비율로, -XX:+NewRatio=1로 지정하면 비율은 1:1이 됩니다. NewRatio가 2이면 비율은 1:2가 되며, 값이 커질수록 Old 영역의 크기가 커지고 New 영역의 크기가 작아집니다. NewRatio 값은 GC의 전반적인 성능에 큰 영향을 줍니다.

 

New 영역의 크기가 작으면 Old 영역으로 넘어가는 메모리 양이 많아져 Full GC도 잦아지고 시간도 오래 걸립니다. 하지만, 경험상 NewRatio의 값이 2나 3일 때 전반적인 GC 성능이 더 좋았습니다. 그러나 이 결과는 객체의 크기 및 생성 주기에 따라 달라지므로, 자신의 서비스 상황에 맞는 값을 찾는 작업이 중요합니다.

 

가장 빠른 GC 튜닝 방법은 성능 테스트를 통해 결과를 비교하는 것입니다. 하지만, 이를 위해 운영 상황과 동일한 부하를 줄 수 있는 환경을 구성하는 것은 쉽지 않습니다. 부하를 주는 URL과 같은 요청 비율도 운영과 동일해야 하지만, 이를 정확하게 구현하는 것은 전문 성능 테스터도 어렵습니다. 그래서 시간이 오래 걸리더라도 운영에 적용하고 결과를 기다리는 것이 더 간단하고 편리합니다.

 

4단계 : GC 튜닝 결과 분석

GC 옵션을 적용하고, -verbosegc 옵션을 지정한 후에 tail 명령어로 로그가 제대로 쌓이고 있는지 확인해야 합니다. 로그가 잘 쌓이고 있다면, 하루 또는 이틀 후에 데이터를 축적한 후 결과를 확인해 보는 것이 좋습니다. 축적된 로그는 로컬로 옮긴 후 HPJMeter로 분석하는 것이 가장 쉽습니다.

 

분석할 때는 다음 사항을 중심으로 살펴보는 것이 좋습니다. 이는 우선 순위 별로 나열되어 있습니다. 

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

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