Post

Optimizing Java - GC 로깅

1. GC 로깅 개요

GC 로그는 시스템이 내려간 원인의 단서를 찾는 콜드 케이스(Cold Case) 분석을 할 때 유용하다. GC 로깅은 사실상 오버헤드가 거의 없으므로 항상 로깅을 활성화하는 것이 좋다.

1) GC 로깅 켜기

GC 로깅을 켜기 위해 필수적인 JVM 플래그는 다음과 같다.

1
2
3
4
5
6
7
8
9
-Xloggc:gc.log -> 로깅할 파일을 gc.log로 지정한다
-XX:+PrintGCDetails -> 메모리 변화량을 포함한 상세 GC 이벤트 정보를 출력한다
-XX:+PrintTenuringDistribution -> 테뉴어드 영역과 관련된(승격 등) 정보를 출력한다
-XX:+PrintGCTimeStamps -> GC 발생 시간을 초 단위로 출력한다.
-XX:+PrintGCDateStamp -> GC 발생 시간을 날짜와 시간 형식으로 출력한다.

Java 9부터 변경
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+phases=debug  

이렇게 세세히 남겨도 오버헤드는 거의 없다고 보면 되므로 일단 기록하는 것이 좋다.

2) GC 로그 VS JMX

JVM 힙 상태를 실시간으로 시각화해주는 툴인 VisualGC는 JMX(Java Management eXtensions) 인터페이스를 통해 JVM 데이터를 수집한다. GC 로그와 JMX의 차이는 다음과 같다.

  • GC 로그 데이터는 GC 이벤트가 발생할 때 기록되지만 JMX는 주기적으로 샘플링하므로 리소스를 더 많이 사용한다
  • GC 로그는 단순하게 로그 파일에 기록하지만 JMX는 원격 메서드 호출(RMI, Remote Method Invocation)을 사용하기 때문에 이 과정에서 네트워크 관련 비용이 발생한다.
  • GC 로그는 다양한 플래그를 활성화하여 상세한 메모리 관리 데이터를 포함하지만 JMX는 제한된 데이터만 제공한다.

따라서 기본적인 힙 사용 현황을 알고 싶을 때는 JMX가 도움이 되지만 GC 이벤트를 자세히 알고 싶을 때는 GC 로그를 활용하는 것이 좋다.

(1) JMX의 단점

JMX는 주기적으로 샘플링을 하지만 가비지 수집이 언제 실행될지 미리 알지 못하므로 각 전후 상황(메모리 상태)를 깊이 있게 기록할 수는 없다. 또한 RMI를 활용하여 데이터를 얻는 간접적인 방식으로 네트워크를 사용함으로써 발생하는 오버헤드를 고려해야 한다.

요약하면, GC 로그는 JMX보다 상세하게 로그를 남길 수 있으며 오버헤드 또한 JMX보다 적다.

(2) GC 로그 데이터 장점

GC 로그는 핫스팟 JVM 내부에서 논블로킹 쓰기 방식을 이용해 남김으로써 애플리케이션 성능에 미치는 영향은 거의 0으로 볼 수 있다. 또한 GC 로그에 쌓인 기초 데이터는 JMX와 달리 특정 GC 이벤트와 연관지을 수 있어서 의미 있는 분석을 수행하기 용이하다.


2. 로그 파싱 툴

GC 로그 메시지는 표준 포맷이 따로 없어 버전에 따라 달라질 수 있다. 따라서 GC 로그 메시지를 직접 파싱하는 작업은 아주 복잡할 수 있기 때문에 GC 로그 파싱은 반드시 툴을 사용 해야 한다.

1) 센섬(Censum)

jClarity사가 제작한 상용 메모리 분석기이다. 애플리케이션의 할당률, 중단 시간 등을 하나의 JVM이 아닌 전체 클러스터 상태를 한눈으로 확인할 수 있다. 이 외에도 다양한 지표를 확인할 수 있다.

  • 할당률
  • 조기 승격
  • 단기적으로 폭발적인 할당
  • 메모리 누수 감지
  • 힙 크기 조정 및 용량 계획

2) GCViewer

GCViewer는 오픈 소스로 기능이 약간 빈약할지라도 간편하게 데이터를 확인할 수 있다. 분석 기능은 따로 지원하지 않고 특정 GC 핫스팟 로그 포맷을 파싱할 수 있다.


3. GC 기본 튜닝

1) GC 기본 튜닝

(1) GC 튜닝 고려사항

GC 튜닝 또한 다른 튜닝 기법 처럼 전체 진단 과정의 일부여야 한다. GC 튜닝을 할 때는 다음과 같은 고려사항이 있다.

  • GC가 원인인지 아닌지 확인하는 행위는 간단하게 할 수 있다.
  • UAT(운영계)에서 GC 플래그를 켜는 것도 오버헤드가 거의 없다.
  • 메모리 프로파일러, 실행 프로파일러를 설정하는 작업은 많은 수고가 필요하다.

따라서 힙, 메모리 할당 등과 관련한 문제를 마주치게 되면 GC가 원인인지 아닌지 간단하게 확인하고 가는 것이 좋다.

(2) GC 튜닝 시 측정해야 할 주요 인자

튜닝을 수행하면서 측정해야 하는 네가지 주요 인자는 다음과 같다.

  • 할당(가장 중요)
  • 중단 민감도
  • 처리율 추이
  • 객체 수명

할당과 관련된 기본 플래그는 다음과 같다.

1
2
-Xms<Size> -> 힙 메모리의 최소 크기를 설정
-Xmx<Size> -> 힙 메모리의 최대 크기를 설정

(3) 플래그를 활용한 튜닝 시 주의해야 할 점

이렇게 플래그를 추가할 때 다음과 같은 점을 명심해야 한다.

  • 한번에 한 플래그씩 추가한다.
  • 각 플래그가 무슨 작용을 하는지 숙지한다.
  • 부수 효과를 일으키는 플래그 조합도 있음을 인지한다.

(4) GC가 원인인지 판단하기

vmstat와 같은 툴로 지표를 체크하고 성능이 떨어진 시스템에 들어가 다음을 확인한다.

  • CPU 사용율이 100%에 가까운지 확인하기
  • 대부분의 시간(90% 이상)이 유저 공간에서 소비되는지 확인하기
  • GC 로그가 자주 쌓이고 있는지 확인하기

이러한 조건이 다 맞는다면 GC가 성능 이슈를 일으키고 있을 가능성이 크므로 자세한 확인과 튜닝이 필요하다.

2) 할당

할당률은 GC를 튜닝하면 성능이 개선될 지 여부를 판단하는데 꼭 필요한 과정이다. 만약 할당률이 1GB/s 이상으로 지나치게 높다면 GC 튜닝으로 해결할 수 있는 문제가 아닌 근본적인 로직에 문제가 있을 수 있다. 예를 들면 아래와 같은 객체가 많은 상황이라면 GC 튜닝보다 로직 개선이 더 중요할 수 있다.

  • 굳이 없어도 그만인 객체를 할당 -> 라이브러리, 프레임워크 중에 이런 경우가 있으므로 잘 찾아서 제거
  • 박싱 비용 -> 찾아서 불필요한 박싱은 최대한 제거
  • 도메인 객체 -> char[], byte[], double[], Map, Object[] 등 메모리를 많이 차지하는 타입 주의
  • 엄청나게 많은 논JDK 프레임워크 객체

도메인 객체가 큰 경우 각 도메인을 쓰레드의 TLAB 공간에 할당함으로써 개선할 수 있다. 이 때 도메인 객체가 지나치게 커서 TLAB 공간에 들어가지 않는 경우 다음과 같은 과정을 수행한다.

  • TLAB에 들어가지 않는 경우 에덴에 직접 할당을 시도한다.
  • 실패하면 영GC를 수행한 후 에덴에 다시 할당을 시도한다.
  • 그래도 실패하면 테뉴어드 영역에 객체를 직접 할당한다.

이 TLAB 크기와 어떤 크기 이상일 때 조기 승격 시킬지에 대한 플래그 또한 존재한다.

1
2
3
-XX:PretenureSizeThreshold=<n> -> 이 상한선을 초과하면 바로 테뉴어드 영역에 할당된다.
-XX:MinTLABSize=<n> -> TLAB 최소 크기를 조정한다.
-XX:MaxTenuringThreshold=<n> -> GC 사이클을 n회 통과하면 테뉴어드 영역으로 승격한다.

만약에 MaxTenuringThreshold가 높으면 조기 승격 문제가 줄어드는 대신 에덴 영역에 머무르는 객체가 증가하여 영GC 횟수가 증가할 수 있다.

3) 중단 시간

중단 시간은 개발자가 생각하는 것만큼 큰 문제가 아닐 수 있다. 인간의 눈은 하나의 데이터 항목을 초당 5회밖에 처리할 수 없어 100 ~ 200밀리초 정도의 중단은 용납할 수 있는 수준일 수도 있다. 비즈니스 요구 사항에 따라 다음과 같이 크게 나눈다면 수집기 선정에 도움이 될 수 있다.

  • 1초 이상 허용: Parallel
  • 100 ~ 1초 허용: 힙 크기가 작다면 Parallel 크다면 G1
  • 100초 이내로 허용: CMS

4) GC 스레드와 GC 루트

GC 루트 탐색 과정에서는 어떤 객체가 생존해야 하고 그렇지 않은지 확인한다. 이 루트 탐색 시간은 다음과 같은 요인에 영향을 받는다.

  • 애플리케이션 스레드 개수
  • 코드 캐시에 쌓인 컴파일된 코드량
  • 힙 크기
  • 적용 가능한 병렬화 정도

애플리케이션 스레드가 많으면 각 스레드의 스택 프레임이 GC 루트로 사용되므로 탐색해야 할 범위가 커지고 모든 애플리케이션 스레드가 세이프포인트에 도달할 때까지 기다려야 하는 등 GC 시간에 영향을 미치게 된다.

스택, 힙 탐색은 비교적 병렬화가 잘 된다. 카드테이블은 올드 객체가 영 객체를 참조하는 정보를 저장하고 있는 자료 구조로 올드 세대 1기가바이트는 2메가바이트의 카드 테이블을 참조한다. 따라서 20기가바이트 힙의 카드 테이블을 탐색할 때는 40메가바이트 정도의 카드 테이블을 확인한다고 생각하면 된다. 이를 시뮬레이션 해보면 카드 테이블 탐색 시간은 10밀리초 정도가 된다.


4. Parallel GC 튜닝

가장 단순한 GC로 튜닝 역시 제일 쉽다. 이 GC의 목표와 트레이드 오프는 다음과 같다.

  • 풀 STW로 콜렉팅
  • GC 처리율이 높고 계산 비용이 싸다
  • 하지만 항상 전체 수집을 하게 된다
  • 중단 시간은 힙 크기에 비례하여 늘어난다.

이 특성이 문제가 되지 않은 소규모의 애플리케이션에서는 Parallel GC가 효과적인 선택이 될 수 있다. 과거에는 영 세대나 서바이버 크기를 직접 조정하는 튜닝이 있었으나 대부분의 상황에서는 프로그램이 알아서 잘 결정하기 때문에 웬만하면 건들지 않는 것이 좋다.


5. CMS 튜닝

CMS는 중단 시간을 최소화하는 수집기이지만 구조가 더욱 복잡해졌고 따라서 튜닝하기 매우 까다롭다. CMS는 중단 시간이 극도로 짧아야하는 요구 사항이 아니라면 좋은 선택이 아닐수도 있다.

CMS 플래그는 매우 많고 이를 모두 이해하고 튜닝하기는 매우 어렵기 때문에 튜닝은 추천되지 않지만 그럼에도 해야한다면 아래 내용을 고려해야 한다.

CMS 수집은 기본적으로 코어 절반이 GC에 할당되므로 중단 시간이 줄어들지언정 애플리케이션 처리율 또한 반토막이 나버린다. 만약 CMS 수집이 끝나자마자 곧바로 새 CMS 수집이 이루어진다면 곧 CMF가 일어날 가능성이 매우 높아진다. 이는 GC가 회수하는 속도가 애플리케이션의 메모리 할당 속도를 따라가지 못해 발생한다.

1
-XX:ConcGCThreads=<n>

해당 플래그를 활용하여 GC에 할당된 코어 수를 절반 이하로 줄여 애플리케이션 로직에 더 많은 리소스를 할당하는 방법도 있지만 CMF에 더 취약해지는 트레이드 오프가 있다.

1
2
-XX:CMSInitiatingOccupancyFraction=<n> -> 힙의 n% 이상이 되면 CMS GC 실행
-XX:+UseCMSInitiatingOccupancyOnly -> CMS GC 자동 조정 비활성화(위의 값을 바탕으로 GC 시작)

n값을 줄이면 GC가 자주 실행되는 대신 CMF가 발생할 위험이 줄어들고 n값이 증가하면 GC가 적게 실행되는 대신 CMF가 발생할 위험이 커진다.

1) 단편화로 인한 CMF

CMS는 올드 세대를 관리할 때 프리 리스트를 사용한다. 이 프리 리스트는 Compaction을 수행하지 않기 때문에 파편화가 발생하고 공간이 있음에도 CMF가 발생할 위험이 있다.

1
-XX:PrintFLSStatistics=1

해당 플래그를 사용하면 평균 블록 크기, 최대 청크 크기를 확인할 수 있고 이를 통해 메모리 청크 크기 분포를 짐작할 수 있다.

  • Free Space: Free List의 총 사용가능한 메모리 크기(파편화는 확인할 수 없음)
  • Max Chunk Size: 파편화되지 않은 여백 중 가장 큰 공간의 크기

따라서 이보다 큰 객체를 테뉴어드로 옮기는 경우 CMF가 일어나게 된다.


6. G1 튜닝

G1 수집기는 할당률이 허락하는 한 리전들에 대한 Compaction을 수행하므로 파편화로 인한 CMF가 일어날 가능성은 없다. G1에서는 할당률이 계속 높은 상태가 유지될 때 조기 승격 문제는 발생할 수 있다. 이를 해결하기 위해 다음 튜닝을 고려할 수 있다.

  • 영 세대를 크게 설정한다.
  • 애플리케이션에서 수용할 수 있는 최장 중단 시간 목표를 설정한다.
  • 테뉴어드로 승격하기 위한 GC 사이클 수를 상향한다.

7. jHiccup

jHiccup은 JVM의 중단 시점을 보여주는 계측 도구이다. 이러한 JVM 중단 시점은 GC STW를 식별하여 GC 튜닝에 활용할 수 있다.


8. GC 모니터링


Reference

This post is licensed under CC BY 4.0 by the author.

© . Some rights reserved.

Using the Chirpy theme for Jekyll.