...
Garbage Collection 튜닝
자바(Java)가 C 언어에 비해 속도 차이가 나는 이유는 아키텍쳐 설계, 즉 JVM에 있는데, 미리 바이너리 코드로 컴파일되는 C언어에 비하여, 자바는 바이트 코드라는 중간 단계 컴파일을 해석하는데 있어서 시간이 소요되기 때문이다. 그리고 무엇보다 자바 어플리케이션 성능의 가장 큰 비중을 차지하는게 바로 가비지 컬렉션(GC)의 Stop-The-World 이다.
이전 포스팅에서 우리는 지금까지 GC의 힙 메모리 구성 및 동작 원리 그리고 GC의 알고리즘 종류에 대해 알아보았다. 그런데 만일 자바 애플리케이션 성능이 제대로 안나온다면 STW를 줄이기 위해 다른 GC 알고리즘 을 채택해서 돌아가게 하면 되지만, 이도 문제가 해결이 안된다면 비로소 GC 튜닝 이라는 것을 해야 한다.
자바에서는System.gc()라는 가비지 컬렉션을 사용자가 직접 실행하는 코드를 지원한다. 그러나 메서드를 호출하는 순간 모든 스레드가 중단되기 때문에 절대 사용해서는 안되는 코드이다.
GC 튜닝의 주의점
그런데 GC 튜닝에 있어서 꼭 명심해야 하는 점이 있다.
첫째는, GC 튜닝 옵션은 서비스의 특징 마다 적정 값이 다르다는 것이다.
예를 들어 '누가 이 옵션값을 썼을 때 성능이 잘 나왔으니 우리도 이렇게 적용하자.' 라고 생각하면 절대 안 된다.
왜냐하면 서비스 주제와 특징마다 생성되는 객체의 크기도 다르고 살아있는 기간도 다르기 때문이다.
따라서 별도의 성능 모니터링을 통해 어느 지점에서 GC의 STW 문제가 나는지 서비스마다 제각각 파악하여 GC 튜닝을 진행할 필요가 있다.
둘째는, GC 튜닝은 가장 마지막에 하는 작업이라는 것이다.
GC 튜닝은 필요한 선행 지식과, 신경써야할 요소, 리스크에 비해서 얻어가는 부분이 너무 적기 때문에, GC를 건드리는 것보다 애플리케이션 코드로써 메모리 최적화를 더 신경 쓸것을 권장하는 의견도 있다.
GC 튜닝을 왜 하는지 근본적인 이유를 생각해보자.
일반적으로 Java에서 생성된 객체는 가비지 컬렉터가 처리해서 지운다.
즉, 생성된 객체가 많으면 많을수록 가비지 컬렉터가 처리해야 하는 대상도 많아지고, GC를 수행하는 횟수도 증가한다는 말이다. 그리고 GC의 수행 횟수가 늘어나면 STW 횟수도 많아지니 성능에 영향이 가게 된다.
따라서 GC 자체를 튜닝 하기 이전에 먼저 코드 단에서 쓸모없는 객체 생성을 줄이는 리팩토링 작업을 먼저 하는 것이 근본적인 해결법이 된다.
예를들어 String 대신 StringBuilder나 StringBuffer를 사용하는 거나, 로그를 최대한 적게 쌓도록 하는 등 임시 메모리를 적게 사용하도록 해주는 것이 먼저이다.
그리고 리팩토링 작업과 어플리케이션 메모리 조정을 해도 여전히 성능에 문제가 생기면 그제서야 GC 튜닝을 진행한다.
다시 말하자면 GC 튜닝은 자바 어플리케이션 성능 향상 작업에서 가장 마지막에 하는 작업이라는 것을 잊지 말자.
GC 튜닝의 목표
GC 튜닝이란, 성능상 이슈를 일으키기 쉬운 GC에 대하여 최적화를 진행 하는 것을 말한다.
GC 튜닝의 목표의 핵심은 Minor GC 보다 Stop-The-Wold 시간이 긴 Major GC의 관리이다.
첫째는, Old 영역으로 넘어가는 객체의 수 최소화하기 이다.
위에서 살펴봤듯이, Old 영역의 크기는 Young(New) 영역의 크기보다 훨씬 거대하다.
따라서 Old 영역의 GC는 Young(New) 영역의 GC에 비해 상대적으로 시간이 오래 소요되기 때문에, 애초에 Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 많이 줄일 수 있게 된다.
이말은 즉, Young(New) 영역의 크기를 잘 조절하여 Old 영역으로 넘어가는 빈도를 줄이면 큰 효과를 볼 수 있다는 뜻이다.
둘째는, Full GC 시간 줄이기 이다.
Full GC의 실행 시간은 상대적으로 Minor GC에 비해 길기 때문에, Old 영역의 크기를 적절하게 설정하는 것도 하나의 방법이다.
그렇다고 Old 영역의 크기를 막 줄여버리면 자칫 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어날 수도 있다.
반대로 Old 영역의 크기를 늘리면 Full GC 횟수는 줄어들지만 실행 시간이 늘어나게 된다.
즉, GC 튜닝의 관건은 이 둘 사이를 잘 아우르는 적정 범위를 찾는 것이라 할 수 있다.
GC 튜닝 절차 맛보기
1. GC 상황 모니터링
GC 상황을 모니터링하며 현재 운영되는 시스템의 GC 상황을 확인한다.
# jstat gcutil 명령어로 현재 실행중인 8884번 프로세스에 대해 1초에 한번 씩 총 10번 GC와 관련된 정보를 출력하도록 모니터링
jstat -gcutil -t 8844 1000 0
jstat 명령은 JDK 1.6 버전부터 함께 제공되기 시작한 기본 모니터링 및 분석툴이다.
이외에도, 'jconsole', 'jvisualvm', 'Visual GC', 'gceasy.io' 등 다양한 java GUI GC 모니터링 분석 툴이 존재한다.
컬럼 | 설명 |
S0 | Survivor 영역 0의 사용율(현재 용량에 대한 비율) |
S1 | Survivor 영역 1의 사용율(현재 용량에 대한 비율) |
E | Eden 영역의 사용율 (현재 용량에 대한 비율) |
O | Old 영역의 사용율 (현재 용량에 대한 비율) |
P | Permanent 영역의 사용율 (현재 용량에 대한 비율) |
YGC | Young 세대의 GC 이벤트 수 |
YGCT | Young 세대의 GC 시간 |
FGC | Full GC 이벤트 수 |
FGCT | Full GC 시간 |
GCT | GC 총 시간 |
2. 모니터링 결과 분석 후 GC 튜닝 여부 결정
GC 상황을 확인한 후에는, 결과를 분석하고 GC 튜닝 여부를 결정해야 한다.
- Minor GC 수행시간 : YGCT / YGC (0.314 / 19) = 0.016초
- Major GC 수행 시간 : FGCT / FGC (0.291 / 3) = 0.097초
만약 모니터링 결과가 다음의 조건에 모두 부합한다면, GC 튜닝이 굳이 필요하지는 않다.
- Minor GC의 처리 시간이 빠르다 (50ms 내외)
- Minor GC의 주기가 빈번하지 않다 (10초 내외)
- Full GC의 처리 시간이 빠르다 (1초 내외)
- Full GC의 주기가 빈번하지 않다 (10분에 1회)
3. GC 알고리즘 방식 지정
위의 모니터링 결과를 보고, GC 튜닝을 진행하기로 결정했다면 GC 알고리즘 방식을 선정한다.
이때 서버가 여러 대이면 서버에 GC 옵션들을 서로 다르게 각각 지정해서, 현재 내 어플리케이션의 GC 알고리즘에 따른 차이를 확인하는 것이 좋다. (알고리즘이 더 최신이라고 해서 반드시 내 어플리케이션에서 해당 GC 방식이 적절하다라고 보증 할수 없다)
GC 알고리즘 | 내용 |
Parallel GC | - '처리량'이 중요한 시스템에서 주로 사용 - Full GC 수행 시 compaction 작업이 수행되기 때문에 GC 시간 자체는 많이 소요되나 일정한 멈춤 시간을 제공함 |
CMS GC | - 응답시간이 중용한 시스템에사 주로 사용 - compaction 미수행으로 Stop-The-World 시간은 짧으나 자주 Compaction이 발생하는 시스템의 경우 오히려 Full GC 보다 Compation 시간이 오래 걸릴 수 있음 - 자원 사용량이 증가하는 점도 고려해야 함 |
G1 GC | - 성능적으로 가장 우수한 GC 방식이나, JDK 7 버전부터 정식 제공되었으며, Java 9 에서 Default GC 방식으로 채택 |
4. 힙 메모리 크기 지정
JVM의 힙 메모리는 크기에 따라 GC 발생 횟수와 수행 시간에 영향을 끼치기 때문에 옵션을 통해 조절하면 어플리케이션 성능 향상 효과를 가져올 수 있다.
여기서 말하는 메모리 크기는 JVM의 시작 크기(-Xms)와 최대 크기(-Xmx)를 말한다.
메모리 크기와 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 성능에 많은 차이가 발생한다.
NewRatio는 New 영역과 Old 영역의 비율이다.
-XX:+NewRatio=1로 지정하면 (New 영역):(Old 영역)의 비율은 1:1이 된다.
만약 1GB라면 (New 영역):(Old 영역)은 500MB:500MB가 된다.
NewRatio가 2이면 (New 영역):(Old 영역)이 1:2가 된다.
즉, 값이 커지면 커질수록 Old 영역의 크기가 커지고 New 영역의 크기가 작아진다.
# 힙 시작 크기 256mb, 힙 최대 크기 2gb
# young 영역과 old 영역 비율 1:2 로 설정 (New 영역:Old 영역 = 1:2)
# Parallel GC 로 실행
java -Xms256m -Xmx2048m -XX:+NewRatio=2 -XX:+UseParallelGC
5. 튜닝 결과 분석
GC 옵션을 지정하고 24시간 이상(하루, 이틀) 데이터를 수집한다.
그리고 로그를 분석해 메모리가 어떻게 할당되는지 확인한다.
그 다음에 GC 방식 / 메모리 크기를 변경해 가면서 최적의 옵션을 찾아 나가면 된다.
분석할 때는 다음의 사항을 중심으로 살펴보는 것이 좋다. 이는 우선 순위 별로 나열되어 있다.
- FullGC 수행 시간
- MinorGC 수행 시간
- Full GC 수행 간격
- MinorGC 수행 간격
- 전체 Full GC 수행 시간
- 전체 Minor GC 수행 시간
- 전체 GC 수행 시간
- Full GC 수행 횟수
- Minor GC 수행 횟수
6. 전체 서버에 반영 및 종료
GC 튜닝 결과가 만족스러우면 전체 서버에 GC 옵션을 적용하고 마무리한다.
이렇게 가비지 컬렉션을 어떤 기준으로 어떤 절차로 튜닝하는지 전반적인 과정을 알아보았다.
이 포스팅에서는 실전 GC 튜닝까지는 깊이 다루지는 않는다.
주니어 레벨 기준으로 너무 고난이도 내용이며, 튜닝할 테스트 환경 구성도 준비하기 힘들며, 무엇보다 실무에서 GC 튜닝을 할일이 거의 없기 때문이다. 그러나 만일 GC 튜닝에 대해 정말 관심이 있다면, 아래 참고자료에서 네이버 기술블로그에서 작성한 튜닝 방법을 참고하길 바란다.
# 참고자료
https://d2.naver.com/helloworld/37111
https://d2.naver.com/helloworld/6043
https://www.youtube.com/watch?v=FMUpVA0Vvjw
https://daemonthreadscom.wordpress.com/java-2/garbage-collection-in-java/jvm-arguments-in-gc/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.