책을 읽겠습니다!/자바 성능 튜닝 이야기

[자바 성능 튜닝 이야기] GC는 언제 발생할까 / GC가 어떻게 수행되고 있는지 보고 싶다?

Unagi_zoso 2023. 9. 17. 03:04

HotSpot gc로 검색하면 좋은 정보가 많다.
JDK 6 기준의 이야기다. 반드시 최신 정보도 공부해야한다.

GC의 역할은 더 이상 필요 없는 객체를 처리한다.

Runtime data area

자바에서 사용하는 메모리 영역

  • PC 레지스터
  • JVM 스택
  • 메서드 영역
  • 런타임 상수 풀
  • 네이티브 메서드 스택

이 영역 중 GC의 대상이 되는 건 힙 영역

GC에 대해 공부하는 부분이니 GC에 의해 관리되는 Heap이냐 Non-Heap이냐로 보자.

Heap 메모리

클래스 인스턴스, 배열이 이 메모리에 쌓인다. 이 메모리는 '공유(shared) 메모리'라고도 불리우며 여러 스레드에서 공유하는 데이터들이 저장되는 메모리다.

Non-Heap 메모리

이 메모리는 자바의 내부 처리를 위해서 필요한 영역이다. 여기서 주된 영역이 바로 메서드 영역이다.

  • 메서드 영역: 메서드 영역은 모든 JVM 스레드에서 공유한다. 이 영역에 저장되는 데이터들은 다음과 같다.
    • 런타임 상수 풀: 자바의 클래스 파일에는 constant_pool이라는 정보가 포함되어 있따. 이 constatnt_poo에 대한 정보를 실행 시에 참조하기 위한 영역이다. 실제 상수 값도 여기에 포함될 수 있지만, 실행 시에 변하게 되는 필드 참조 정보도 포함딘다.
    • 필드 정보에는 메서드 데이터, 메서드와 생성자 코드가 있다.
  • JVM 스택: 스레드가 시작할 때 JVM 스택이 생성된다. 이 스택에는 메서드가 호출되는 정보인 프레임이 저장된다. 그리고, 지역 변수와 임시 결과, 메서드 수행과 리턴에 관련되 정보들도 포함된다.
  • 네이티브 메서드 스택: 자바 코드가 아닌 다른 언어로 된(보통은 C로 된) 코드들이 실행하게 될때의 스택 정보를 관리한다. 스택이니 스레드마다 생성된다.
  • PC 레지스터: 자바의 스레드들은 각자의 PC 레지스터를 갖는다. 네이티브한 코드를 제외한 모든 자바 코드들이 수행될 때 JVM의 인스트럭션 주소를 pc 레지스터에 보관한다.

여기서 Heap 영역과 메서드 영역은 JVM이 시작될 때 생성된다.

공유되는 자원은 Method area, Heap
Method area에 클래스 자체의 정보가 저장되며 그 내부엔 Runtime Constant Pool, Method Code, Attributes and Field Variables가 있다.

Heap에는 클래스 인스턴스들의 정보가 저장된다. array 또한 내부 데이터들은 객체처럼 이 곳에 저장된다.

thread 하나 당 PC Register 들을 가져서 다른 자바 Instruction을 가리키고, JVM Stack을 가져서 함수가 수행될 때 프레임이 생성되 그 안엔 그 지역 변수나, Operand Stack, RCP reference를 가지고 있다. 스레드에서 사용되는 네이티브 메서드들은 Native Method Stack을 사용된다.

JVM의 메모리 구조는 이러하지만 이번에 다룰 것은 CG이니 힙을 중점적으로 보자. Method area도 공유되는 자원이고 클래스 변수 같은건 여기 저장될 거 같은데 GC의 영향을 받지 않는 듯 하다. 최신 버전에 와선 이러한 부분에 변경이 있다 들었다. 최신 버전 변경사항은 꼭 숙지하자.

GC의 원리

가비지 콜렉터의 역할

  • 메모리 할당
  • 사용 중인 메모리 인식
  • 사용하지 않는 메모리 인식

사용하지 않는 메모리 인식하느 작업을 하지 않으면 , 할당한 메모리 영역이 꽉 차서 JVM에서 행(Hang)이 걸리거나 , 더 많은 메모리를 할당하려는 현상이 발생할 것이다. JVM의 최대 메모리 크기를 지정해서 전부 사용한다음, GC를 해도 더 이상 사용 가능한 메모리 영역이 없는데 계속 메모리를 할당하려고 하면 OutOfMemoryError가 발생하여 JVM이 다운될 수 있다.

  • 행(Hang)이란 서버가 요청을 처리 못하고 있는 상태를 의미한다.

자바의 메모리 영역..

이 메모리 영역도 최근 버전에 있어서 변화가 있는 것으로 알고있다. 반드시 최신 정보를 살펴보고 숙지하자

Perm 영역은 없는걸로 치자. 거의 사용이 되지 않는 영역으로 클래스와 메서드 정보와 같이 자바 언어 레벨에서 사용하는 영역이 아니다.
JDK8부터는 이 영역이 사라진다.(metaspace라는 영역으로 대체, 이 책이 쓰여진 시점에도 JDK 8 스펙이 어느 정도 알려졌던걸까 ) Virtual이라고 쓰여 있는 부분 또한 가상 영역이므로 고려하지 말자. Constant pool은 JDK 8부터 metaspace 공간에 두며 metaspace 영역은 힙에 속하게 된다.

눈 여겨볼건 Young 영역과 Old 영역 일부가 남는다. Young 영역은 다시 Eden 영역 및 두 개의 Survivor 영역으로 나뉘므로 우리가 고려할 것은 4개 영역으로 나뉜다.

Young 영역 Old 영역
Eden Survivor1 Survivor2 메모리 영역

Perm 영역에는 클래스와 메서드 정보 이외에도 intern 된 String 정보도 포함하고 있다. (intern된 String은 뭘까.. String 클래스에는 intern()이라는 메서드가 존재한다. 이 메서드를 호출하면 해당 문자열의 값을 바탕으로 한 단순 비교가 가능하다. 즉, 참조 자료형은 equals()메서드로 비교해야하지만, intern() 메서드가 호출된 문자열들은 == 비교가 가능하다. 값 비교 성능은 빨라지지만, 문자열 정보들이 Perm 영역에 들어가기 때문에 Perm 영역의 GC가 발생하는 원인이 되기도 한다. 8이후부턴 사라지니 이런 일 일어나지 않을 것.

일단 메모리에 객체가 생성되면, 아래 그림의 가장 왼쪽인 Eden 영역에 객체가 지정된다.

Eden 영역에 데이터가 꽉 차면, 이 영역에 있던 객체가 어디론가 옮겨지거나 삭제 되어야 한다. 이 때 옮겨가는 위치가 Survivor 영역이다. Survivor 사이에는 우선 순위가 없다. 1, 2 영역 중 한 영역은 반드시 비어 있어야 한다. 그 비어 있는 영역에 Eden 영역에 있던 객체 중 GC 후 살아남은 객체들이 이동한다.

이와 같이 Eden 영역에 잇던 객체는 Survivor 영역의 둘 중 하나에 할당된다. 할당된 Survivor 영역이 차면, GC가 되면서 Eden 영역에 있는 객체와 꽉 찬 Survivor 영역에 있는 객체가 비어 있는 Survivor 영역으로 이동한다. 이러한 작업을 반복하면서, Survivor 1, 2 를 왔다갔다하던 객체들은 Old 영역으로 이동한다. 그리고, Young 영역에서 Old 영역으로 넘어가는 객체 중 Survivor 영역을 거치지 않고 바로 Old 영역으로 이동하는 객체가 있을 수 있다. 객체의 크기가 아주 큰 경우인데, 예를 들어 Survivor 영역의 크기가 16MB인데 20MB를 점유하는 개체가 Eden 영역에서 생성되면 Survivor 영역으로 옮겨갈 수 없다. 이런 객체들은 바로 Old 영역으로 이동하게 된다.
Old는 크다!

GC의 종류

GC는 Minor GC, Major GC가 있다.

  • Minor GC: Young 영역에서 발생하는 GC
  • Major GC: Old 영역이나 Perm 영역에서 발생하는 GC

이 두 가지 GC가 어떻게 상호 작용하느냐에 따라 GC 방식에 차이가 나며 성능에도 영향을 준다.
GC가 발생하거나 객체가 각 영역에서 다른 영역으로 이동할 때 애플리케이션의 병목이 발생하면서 성능에 영향을 주게 된다.
그래서 HotSpot JVM에서는 스레드 로컬 할당 버퍼(THABs: Thread-Local Allocation Buffers)라는 것을 사용한다. 이를 통해 각 스레드별 메모리 버퍼를 사용하면 다른 스레드에 영향을 주지 않는 메모리 할당 작업이 가능해진다. 스레드 별로 힙 영역을 땅가르기 한다는 설명이 있다.
이로 인해 서로의 영역을 간섭하지 않고 GC의 수행에도 고려할 복잡할 상황을 배제해준다.

5가지 GC 방식

JDK 7이상에서 지원하는 GC 방식에는 다섯 가지가 있다.

  • Serial Collector(이하 시리얼 콜렉터)
  • Parallel Collector(이하 병렬 콜렉터)
  • Parallel COmpacting Collector(이하 병렬 콤팩팅 콜렉터)
  • Concurrent Mark-Sweep (CMS) Collector (이하 CMS 콜렉터)
  • Garbage First Collector (이하 G1 콜렉터)

여기 명시된 다섯 가지의 GC 방식은 WS나 자바 애플리케이션 수행 시 옵션을 지정하여 선택할 수 있다. 그런데, G1 콜렉터는 JDk 7부터 정식으로 사용할 수 있다.

시리얼 콜렉터

Young 영역과 Old 영역이 시리얼(연속적으로) 처리되며 하나의 CPU를 사용한다. sun에서 이 처리를 수행할 떄를 Stop-the-world라 표현한다.
즉 콜렉션이 수행될 때 애플리케이션 수행이 정지된다.

1) 일단 살아 있는 객체들은 Eden 영역에 있다.
2) Eden 영역이 꽉 차게 되면 To Survivor 영역(빈 곳)으로 살아있는 객체가 이동한다. 이 때 survivor 영역에 들어가기 너무 큰 객체는 바로 Old 영역으로 이동한다. 그리고 From Survivor 영역에 있는 살아 있는 객체는 To Survivor 영역으로 이동한다.

트리거는 Eden이 다차는가 이다.

3) To Survivor 영역이 꽉 찼을 경우, Eden 영역이나 From Survivor 영역에 남아있는 객체들은 Old 영역으로 이동으로 이동. (가능한 빈 Sjrvivor에 담다가 그마져도 넘치면 Old로 이동한다.)

이후 Old 영역이나 Perm 영역에 있는 객체들은 Mark-sweep-compact 콜렉션 알고리즘을 따른다. 이 알고리즘에 대해 간단히 말하자면 쓰이지 않는 개체를 표시해서 삭제하고 한 곳으로 모으는 알고리즘이다. 이름 그대로네.. 마킹해서 쓸어내고 한 곳으로 압축..

1) Old 영역으로 이동된 객체들 중 살아있는 객체를 식별한다(표시단계)
2) Old 영역의 객체드을 훑는 작업을 수행하여 쓰레기 객체를 식별한다(스윕 단계),
3) 필요 없는 객체들을 지우고 살아있는 객체들을 한 곳으로 모은다(컴팩션 단계).

근데 굳이 Stop the world할 필요가 있을까? 메모리스왑인 아웃 떄문인가?메모리에 있는 내용들도 실시간으로 바뀔 수 있으니 정지를 시키는건가? 이건 OS 레이어에서 다루는 부부인데 JVM만 스탑한다고 되는건 아닌거 같고.. 아.. 애플리케이션 스레드를 안 멈춘다는 건 수시로 jvm의 멤리에 데이터가 올라간다는건데 그럼 충돌이 있겠구나...

이렇게 작동하는 시리얼 콜렉터는 일반적으로 클라이언트 종류의 장비에서 많이 사용된다. 다시 말해, 대기 시간이 많아도 크게 문제되지 않는 시스템에서 사용된다는 의미이다. ㅅ리얼 콜렉터를 명시적으로 지정하려면 자바 명령 옵션에 -XX:+UseSerialGC를 지정하면 딘다.

병렬 콜렉터

스루풋 콜렉터(throughput collector)로도 알려진 방식. 목표는 다른 CPU가 대기 상태로 남아 있는 것을 최소화하는 것이다. 시리얼과 달리 Young 영역에서의 콜렉션을 병렬로 처리한다.(스탑 더 월드 의 시간이 줄어든다.)
많은 CPU를 사용하기에 GC의 부하를 줄이고 애플리케이션의 처리량을 증가시킬 수 있다.
Old 영역의 GC는 시리얼 콜렉터와 마찬가지로 Mark-sweep-compact 콜렉션을 사용한다. 이 방법으로 GC를 하도록 명시적으로 지정하려면 -XX:+UseParallelGC옵션을 자바 명령 옵션에 추가하면 된다.

병렬 콤팩팅 콜렉터

JDK 5.0 업데이트 6부터 사용 가능하다. 병렬 콜레겉와 다른 점은 Old 영역 GC에 다른 알고리즘이 사용된다.

  • 표시 단계: 살아있는 객체를 식별하여 표시해 놓는 단계
  • 종합 단계: 이전에 GC를 수행하여 컴팩션된 영역에 살아 있는 객체의 위치를 조사하는 단계
  • 컴팩션 단계: 컴팩션을 수행하는 단계. 수행 이후에는 컴팩션된 영역과 비어있는 영역으로 나뉜다.

여러 CPU를 사용하는 서버에 적합하다. GC를 사용하는 스레드 개수는 -XX:ParallelGCThreads=n 옵션으로 조정할 수 있다.
이 방식을 사용하려면 -XX:+UseParallelOldGC 옵션을 자바 명령 옵션에 추가하면 된다.

  • 시리얼 콜렉터, 병렬 콜렉터,... 병렬 콤팩팅 콜렉터의 Old영역 콜렉팅 차이
    스윕, 종합 단게의 차이
  • 스윕 단계는 단일 스레드가 Old 영역 전체를 훑는다.
  • 종합 단계는 여러 스레드가 Old 영역을 분리하여 훑는다. 게다가, 앞서 진행된 GC에서 컴팩션된 영역을 별도로 훑는다는 점도 다르다.

CMS 콜렉터

이 방식은 로우 레이턴시 콜렉터로도 알려져 있으며, 힙 메모리 영역의 크기가 클 때 적합하다. Young 영역에 대한 GC는 병렬 콜렉터와 동일하다.
Old 영역의 GC는 다음 단계를 거친다.

  • 초기 표시 단계: 매우 짧은 대기 시간으로 살아 있는 객체를 찾는 단계
  • 컨커런트 표시 단계: 서버 수행과 동시에 살아 있는 객체에 표시를 해 놓는 단계
  • 재표시(remark) 단계: 컨커런트 표시 단계에서 표시하는 동안 변경된 객체에 대해서 다시 표시하는 단계
  • 컨커런트 스윕 단계: 표시되어 있는 쓰레기를 정리하는 단계

컴팩션 단계를 거치지 않는다. 몰아놓지 않아. 빈공간이 발새알 수 있다. -XX:CMSInitiationgOccupancyFraction=n 옵션을 사용하여 Old 영역의 %를 설정한다.

CMS 콜렉터 방식은 2개 이상의 프로세서를 사용하는 서버에 적당하다. 웹 서버가 예다. -XX:+UseConcMarkSweepGC 옵션으로 지정 가능.

CMS 콜렉터는 추가적인 옵션으로 점진적 방식을 지원한다. 이 방식은 Young 영역의 GC를 더 잘게 쪼개어 서버의 대기 시간을 줄일 수 있다. CPU가 많지 않고 시스템의 대기 시간이 짧아야 할 때 사용하면 좋다. 점진적인 GC를 수행하려면 -XX:+CMSIncrementalMode 옵션을 지정하면 된다. 하지만 이 옵션을 지정할 경우 예기치 못한 성능상 저하가 발생할 수 있다. 충분히 테스트를 해보자. 응답시간이 중요한 프로그램에서 쓴다.

G1 콜렉터

Garbage First Collector는 기존 Eden, Survivor 영역으로 나뉘는 Young, Old 영역으로 구성되어 있는데. G1은 좀 다르다. 지금껏 콜렉터들은 모두 Young, Old 주소가 물리적으로 Linear하게 나열되지만 G1은 그렇지 않다.

G1 콜렉터는 다양한 Region(기본 크기 1MB, 최대 32MB)으로 구성된다. 이 개수는 약 2000개 정도된다. 바둑판 모양으로 되어서 각각 Eden, Survivor, Old 영역의 역할을 변경해 가면서 하고, Humongous라는 영역도 포함된다.
G1이 Young GC를 어떻게 하는지 살펴보면 다음과 같다.

1) 몇 개의 구역을 선정하여 Young 영역으로 지정한다.
2) 이 Linear하지 않은 구역에 객체가 생성되면서 데이터가 쌓인다.
3) Young 영역으로 할당된 구역에 데이터가 꽉 차면, GC를 수행한다.
4) GC를 수행하면서 살아있는 객체들만 Survivor 구역으로 이동시킨다.

이렇게 살아남은 객체들이 이동된 구역은 새로운 Survivor 영역이 된다. 그 다음에 Young GC가 발생하면 Survivor 영역에 계속 쌓는다. 그러면서, 몇 번의 aging 작업을 통해서 (Survivor 영역에 있는 객체가 몇 번의 Young GC 후에도 살아 있으면), Old 영역으로 승격된다.

G1의 Old 영역 GC는 CMS GC의 방식과 비슷하며 아래 여섯 단계로 나뉜다.
여기서 STW라고 표시된 단계에서는 전부 Stop the world가 발생한다.

이 방식은 공간적인 이점은 있어보이는데.. 뭐지.. 굳이 이렇게 잘게잘게 쪼갠 이유는..

  • 초기 표시 (Initial Mark) 단계 (STW):Old 영역에 있는 객체에서 Survivor 영역의 객체를 참조하고 있는 객체들을 표시한다.
  • 기본 구역 스캔 (Root region scanning) 단계: Old 영역 참조를 위해서 Survivor 영역을 훑는다. 참고로 이 작업은 Young GC가 발생하기 전에 수행된다.
  • 컨커런트 표시 단계: 전체 힙 영역에 살아있는 객체를 찾는다. 만약 이 때 Young GC가 발생하면 잠시 멈춘다.
  • 재 표시(remark) 단계 (STW): 힙에 있는 살아있는 객체들의 표시 작업을 완료한다. 이 때 snapshot-at-the-beginning (SATB)라는 알고리즘을 사용하며, 이는 CMS GC에서 사용하는 방식보다 빠르다.
  • 청소 (Cleaning) 단계 (StW): 살아있는 객체와 비어 있는 구역을 식별하고, 필요 없는 개체드을 지운다. 그리고 나서 비어 있는 구역을 초기화 한다.
  • 복사 단계(STW): 살아있는 객체들을 비어 있는 구역으로 모은다.

복잡하지만 성능 하나는 빠르다네.. G1은 CMS GC의 단점을 보완하기 위한 것 GC 성능도 매우 빠르다.

너무 복잡하다..

Major, Minor의 종류로 나뉜다하면서 갑자기 별개의 5개의 GC 방식을 소개한다. 어떻게 구성되어있는거지...

강제 GC

System.gc(), Runtime.getRuntime().gc()메서드를 쓰면 딘다. 우리 코드에서 쓸 필요는 잘 없다. 웹에선 절대 쓰지마라.

GC가 어떻게 수행되고 있는 지 알고 싶다

JVM의 상태를 확인하는 명령, 옵션 보자

자바 인스턴스 확인을 위한 jps

jps는 해당 머신에서 운영 중인 JVM의 목록을 보여준다. JDK의 bin 디렉터리에 있다.

jps [-q] [-mlvV] [-Joption] [<hostid>]
  • -q: 클래스나 JAR 파일명, 인수 등을 생략하고 내용을 나타낸다(단지 프로세스 id만 나타난다)

  • -m: main 메서드에 지정한 인수들을 나타낸다.

  • -ㅣ: 애플리케이션의 main 클래스나 애플리케이션 JAR 파일의 전체 경로 이름을 나타낸다.

  • -v: JVM에 전달된 자바 옵션 목록을 나타낸다.

  • -V: JVM의 플래그 파일을 통해 전달된 인수를 나타낸다.

  • -Joption: 자바 옵션을 이 옵션 뒤에 지정할 수 있다.

  • 플래그 파일이란 .hotspotrc의 확장자를 가지거나 자바 옵션에 -XX:Flags=<file name>로 명시한 파일이다. 이 파일을 통해서 JVM의 옵션을 지정할 수 있다.

그냥 아무 옵션 없이 jps를 입력하면 현재 서버에 수행되고 있는 자바 인스턴스들의 목록이 나타난다.

GC 사황을 확인하는 jstat

jstat은 GC가 수행되는 정보를 확인하기 위한 명령어이다. jstat을 사용하면 유닉스 장비에서 vmstat이나 netstat와 같이 라인 단위로 결과를 보여 준다.

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
  • -t: 수행 시간을 표시한다.
  • -h:lines: 각 열의 설명을 지정된 라인 주기로 표시한다.
  • interval: 로그를 남기는 시간의 차이(ms)를 의미한다.
  • count: 로그 남기는 횟수를 의미한다.

jstat 로그 남겨 분석하는데 한계있다. 로그 남기느 주기에 GC가 1번 날 수도 10번 날 수도 있다.. verbosegc 옵션을 권장한다.

닝할 떄 가장 유용한 jstat 옵션 두개

jstat 명령에서 GC 튜닝을 위해 애용하는 옵션 -gcutil과 -gccapacity이다.

jstat -gccapacity pid 

NGC New (Young) 영역의 크기 관련, OCG 로 시작하는 것은 Old 영억 크기 관련, .. 등등 현재 할당된 크기를 나타낸다.. 영역의 크기를 알 수 있으니 어떤 영역의크기를 좀 더 늘리고 줄여야하는지 알 수 있다!

jstat -gcutil 3580 1s

힙 영역의 사용량을 퍼센트로 보여준다.

YGC는 Young 영역의 GC 횟수, YGCT는 Young 영역의 GC가 수행된 누적 시간(초)이다.
Old, Perm 이 두 영역 중 하나라도 GC가 발생하면 FGC의 횟수가 증가하고, FGCT 시간이 올라간다.
GCT는 YGCT와 FGCT 의 합이다.

Young GC가 한 번 수행될 때 시간을구하려면 YGCT / YGC를 계산하면 된다.

CMS GC를 사용할 경우 Full GC의 단계에 따라 수행되는 시간이 다르다. verbosegc를 활용하는 것이 가장 확실하다.

원격으로 JVM 상황 모니터링 jstatd

jstatd[-nr] [-p port] [-n rminame]
  • nr: RMI registry가 존재하지 않을 경우 새로운 RMI 레지스트리를 jstatd 프로세스 내에 시작하지 않는 것을 정의하기 위한 옵션
  • p : RMI 레지스트리를 식별하기 위한 포트번호
  • n : RMI 객체의 이름을 지정한다. 기본 이름은 JStatRemoteHost이다.

그냥 실행하면 안돼 리모트 객체 만드는 것 억제하기에 서버 내 디렉터리 li/security/java.policy 파일에 허가 명령어를 추가해야한다.

리모트 객체를 열고 클라이언트에서 jstat으로 접근하면 된다.

verbosegc 옵션을 이용하여gc 로그 남기기

  • PrintGCTimeStamps 옵션
    verbosegc와 함꼐 사용할수 있는 -XX:+PrintGCTimeStamps 옵션
    언제 GC가 발생하였는지 알 수 있다. 좌측에 서버 기동하고 해당 GC가 수행될 때까지의 시간을 로그에 포함한다.

  • PrintHeapAtGC 옵션

  • XX:+PrintHeapAtGC. 더 많은 정보를 준다.

  • PrintGCDetails
    간결하고 보기 쉬운 옵션을 준다.

GC Analyzer: Sun에서 제공하는 GC 분석 툴.

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

기본적인 메모리 크기 정도만 지정하면 웬만큼 사용량이 많지않은 시스템에서는 튜닝을 할 필요가 없다.

GC 튜닝을 꼭 해야할까?

모든 서비스에서 할 필요는 없다. GC 튜닝이 필요없다는 건 운영 중인 Java 기반 시스템의 옵션에 기본적으로 다음과 같은 것들이 추가되어 있을 때다.

  • -Xms 옵션과 -Xmx 옵션으로 메모리 크기를 지정했다.
  • -server 옵션이 포함되어 있다.

그리고 시스템의 로그에는 타임아웃 관련 로그가 남아있지 않아야 한다.

  • DB 작업과 관련된 타임아웃 (정상적 응답을 받지 못함)

  • 다른 서버와의 통신 시 타임 아웃 (통신 문제나 원격 ㅅ버의 성능 문제 또는 GC 때문일 수도 있음)

  • JVM의 메모리 크기도 지정하지 않았고,

  • Timeout이 지속적으로 발생하고 있다면

GC 튜닝을 고려하자.
GC 튜닝은 가장 마지막에 하는 작업이다. 이걸 하는 근본적인 이유를 보자. Java의 객체는 가비지 컬렉터가 처리한다. 생성된 객체가 많을 수록 가비지 컬렉터가 처리해야하는 대상도 많하지고 GC를 수행하는 횟수도 증가한다. 그러니 객체 생성을 줄이는 작업을 우선 시 하자.
String 대신 StringBuilder, StringBuffer를 사용하자. 로그를 최대한 적게 쌓도록 하는 등 임시 메모리를 적게 사용하도록 하는 작업은 중요하다.
어플리케이션 메모리 사용도 튜닝을 많이 해서 어느 정도 만족할만한 상황이 되었다면 GC튜닝을 시작하자. GC 튜닝의 목적을 두 가지로 나눈다. Old 영역으로 넘어가는 객체의 수를 최소화하는 것과 Full GC의 실행 시간을 줄이는 것이다.

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

Oracle JVM 에서 제공하는 모든 GC는 Generational GC이다. 즉, Eden 영역에서객체가 처음 만들어지고 Survivor 영역을 오가다가,끝까지 남아있는 객체는 Old 영역으로 이동한다. (G1이 경우 약간 상이하게 동작). Eden에서 크기가 커져 Old 영역으로 바로 넘어가는 객체도 있다.
Old 영역의 GC는 New 영역의 GC에 비해 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 많이 줄일 수있다. Old 영역으로 넘어가는 객체의 수를 줄인다는 말을 잘못 이해하면 객체를 마음대로 New 영역에만 남길 수 있다고 생각할 수 있는데, 그렇게는 할 수 없다. 하지만 New 영역의 크기를 잘 조절함으로써 큰 효과를 볼 수는 있다.

Full GC 시간 줄이기

Full GC의 수행 시간은 상대적으로 Young GC에 비하면 길다. 그래서 FUll GC 실행 시간이 오래 소요되면 (1초 이상) 연계된 여러 부분에서 타임아웃이 발생할 수 있다. 그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 OutOfMemoryError가 발생하거나 Full GC회수가 늘어난다. 반대로 Old 영역의 크기를 늘리면 Full GC 회수는 줄어들지만 실행 시간이 늘어난다. Old 크기를 적절하게 '잘' 설정해야한다.
가장 베스트는 GC가 일어나지 않게 메모리 크기를 결정하는 것 같다. 이러긴 힘드니 Old 영역을 확장함으로 GC의 실행횟수가 줄어드는 이점과 Old 영역을 확장함으로 GC 수행 시 확인해야하는 메모리가 늘어남으로 수행시간이 증가하는 단점을 잘 저울질 해서 설정해야할 듯 하다

GC의 성능을 결정하는 옵션들

일단 GC 여러 개 시험해보고 성능 테스트 해본 후 결정하는게 제일 좋다.

  • 힙영역의 크기

  • -Xms : JVM 시작 시 힙 영역의 크기

  • -Xmx : 최대 힙 영역 크기

  • New 영역의 크기

  • -XX:NewRatio : New영역과 Old영역의 비율

  • -XX:NewSize : New 영역의 크기

  • -XX:SurvivorRatio : Eden 영역과 Survivor 영역의 비율

필자가 좋아하는 옵션은 -Xms, -Xmx, -XX:NewRatio 옵션이다.

특히 NewRatio 잘 쓰면 성능 차이기대 가능하다.

Perm은 과련 문제 생기면 고려하자.

  • GC 방식 옵션 (JDK 6.0 기준)

  • Serial GC : -XX:+UseSerialGC

  • Parallel GC : -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 ||JDK6에서는 두 옵션을 반드시 같이 사용해야한다.
    : -XX:+UseG1GC

GC 방식 중 특별히 신경 쓸 필요가 없는건 Serial이다. Serial 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 옵션에 따른 차이를 확인해야한다.

4) 결과 분석
적어도 24시간 이상 데이터를 수집한 후 분석을 실시한다. 운이 좋다면 가장 적합한 GC 옵션을 찾을 수 있다. 그렇지 않다면 로그를 분석해 메모리가 어떻게 할당되는지 확인해야한다.

5) 만족스럽다면 전체 서버에 반영 및 종료
잘못하면 장애로 이어질 수 있으니 배포할 때 처럼 두렵다.

궁금한거

Full GC는 다음에 발생

  • Old 영역이 가득 찼을 때
  • metaspace, Perm 영역이 가득 찼을 때
  • API로 호출 시

Major GC, Minor GC 속도 차이나는 이유
Minor GC는 Young Gen에서 빠르게 생성되었다가 사라지는 객체들을 주로 다룬다. 비교적 짧은 주기로 시행되기에 빠르게 들어왔다 빠르게 제거한다.
Major GC는 Old Gen에서 일어나는데 오랫동안 살아있는 객체들을 다루기에 빠르게 사라지지 않고 계속해서 살아남아있는 대량의 객체들을 처리하기에 시간이 많이 걸린다.

그리고 Major GC, Full GC는 모든 힙영역을 대상으로 작동하며 Minor CG는 Young 영역을 대상으로 진행된다.
그리고 Major, Full GC는 전체 STW할 수 있다.Minor는 이게 좀 더 짧다.

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

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

이 때 데이터를 보는 순서는 다음과 같다.

1) YGC와 YGCT의 값을 확인한다.
YGCT값을 YCG로 나누면 YGC의 수행 시간이 나오는데 예시에서는 50ms로 나온다 이 정도면 Young 영역의 GC는 신경 쓰지 않아도 된다.

2) FGCT와 FGC의 값을 확인한다.
FGCT 값을 FGC로 나누면 19.68초가 나온다. FGC의 수만큼 평균을 구한 것이기에 분산을 따져야하지만 어떠한 경우를 고려해도 GC 튜닝이 필요해 보인다.
이렇게 GC의 상홍을 jstat으로 간단하게 확인할 수 있지만, -verbosegc 옵션으로 로그를 남겨 분석하는 것이 가장 좋다. -verbosegc 로그를 분석하는 도구중 HPJMeter를가장 좋아한다 한다. 사용법도 간단하고 분석하는 방법도 어렵지 않다한다. 이를 사용하면 GC를 수행한 시간 분포와 얼마나 자주 GC가 발생하는지를 쉽게 확인할 수 있다.

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

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

시간 기준은 서비스의 상황에 따라 달라질 수 있다. 한 가지 주의할 점은 Minor GC와 Full GC의 시간만 보면 안 된다는 점이다. GC가 수행되는 횟수도 확인해야한다. 만약 New 영역의 크기가 너무 작게 잡혀있다면 Minor GC가 발생하는 빈도가 매우 높을 뿐만 아니라 (1초에 한 번 이상인 경우도 잇음), Old 영역으로 넘어가는 객체의 개수도 증가하게 되어 Full GC 횟수도 증가한다. 따라서 jstat 명령의 -gccapacity 옵션을 적용하여 각 영역을 얼마나 점유하여 사용하는지도 확인해야한다.

3-1 단계: GC 방식 지정

이제 튜닝에 들어가서 GC 방식과 메모리 크기를 지정하는 방법을 살펴보자.
Serial GC는 운영에서사용하기 힘들다. CMS GC의 FUll GC 처리 시간은 빠르지만, Concurrent mode failure가 발생하면 다른 Parallel GC보다 느려진다. 쓸 수 있는 GC는 다 써서 하나 고르자.

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

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

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

CMS GC는 기본적으로 압축 작업을 수행하지 않기 때문에 당연히 속도는 더 빠르다. 하지만 압축 작업을 수행하지 않으면 디스크 조각 몽므을 실행하기 전의 상태처럼 빈 공간이 여기저기생긴다. 단편화 때문에 담을 공간이 없으면 Concurrent mode failure라는 경고가 발생하면서 압축 작업을 수행한다. 그런데 CMS GC를 사용할 때는 압축 시간이 다른 Parallel GC보다 더 오래 소요된다. 여러개로 테스트해서 최적 찾자.

3-2 단계: 메모리 크기

여기서 말하는 메모리 크기는 JVM의 시작 크기(-Xms)와 최대 크기(-Xmx)를 말한다. 메모리크기와 C 발생 횟수, GC 수행 시간의 관계는 다음과 같다.

  • 메모리 크기가 크면,
    • GC 발생 횟수는 감소한다.
    • GC 수행 시간은 길어진다.
  • 메모리 크기가 작으면,
    • GC 발생 횟수는 증가한다.
    • GC 수행 시간은 짧아진다.

자원이 좋은 시스템이라서 메모리를 10GB로 설정했는데 Full GC가 1초 이내로 끝난다면 10GB로 해도 좋다. 하지만 보통 10GB로 잡으면 Full GC 시간이 10 ~ 30초 정도 소요된다. 그렇다면 메모리 크기를 어떻게 설정해야할까? 보통 500MB로 설정하라고 이야기 한다. 그렇다고 WAS의 메모리를 -Xms 500m 옵션과 -Xmx500m 옵션으로 지정하라는 이야기는 아니다. GC 튜닝 이전에 현재 상황을 모니터링한 결과를 바탕으로 Full GC가 발생한 이후에 남아있는 메모리의 크기를 봐야한다. 만약 Full GC 후 남아 있는 Old 영역의 메모리가 300MB 정도라면 300MB(기본) + 500MB(Old 최소) + 200MB (여유)를 감안해 Old 만 1GB 정도 지정해야한다. 그래서 3 대정도 운영 서버가 있다면 한 대는 1 다른앤 1.5 다른앤 2로 지정해 결과를 지켜보고 결정한다.

메모리 크기가 작다고 무조건 빠른건 아니야. 서버의 성능에 따라 다르고, 객체의크기에 따라 시간이 달라지기 때문이다.
그러니 측정 데이터셋을 최대한 많이 만들어 모니터링을 통해 확인하는 것이 가장 좋다.

메모리 크기 지정 시 해야하는 게 하나 더 있다. NewRatio다. new와 Old의 비율이다. -XX:NewRatio=1로 지정하면 new old의 이뷸은 1:1이 된다.
NewRatio=2이면 1:2가 된다. NewRatio 값은 GC의 전반적인 성능에 맣은 영향을 준다. New 영역의 크기가 작으면 Old 영역으로 넘어가는메모리의 양이 많아져서Full GC도 잦아지고 시간도 오래걸린다.

경험상 2, 3일 때 전반적인 GC 성능이 더 좋았다. 정확하게는 TPS가더 높게 나옸다. 하지만, 이 결과는 객체의 크기 및 생성 주기에 따라 달라지기에 자신이 운영하는서비스의상황에 맞는 값을 찾는 작업을 수행하는 것이 가장 중요하다.

GC 튜닝을 가장 빨리 진행하는 방법은 역시성능 테스트로 결과를 비교하는 것이다. 테스트환경 구성하기 힘드니 시간이 오래 걸리더라도운영에 적용하고 결과를 기다리는 것이 간단하고 편하다.. 요건 좀 무섭네요 실제 환경에 그런 기능을 넣으라니

4 단계: GC 튜닝 결과분석

GC 옵션을 적용하고, -verbosegc 옵션을 지정한 다음에 tail 명령어로 로그가 제대로 쌓이고 있는지 확인해야 한다. 축적된 로그는 로컬 PC로 옮기고 HPJMeter 같은걸로 분석하자.
분석할 때는 다음의 사항을 중심으로 살펴보자

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