[Java] Java Garbage Collection

2021. 4. 22. 18:38Language/Java

1. Java Garbage Collection

GC는 다음과 같은 작업을 수행한다.

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

즉, 메모리가 부족할 때 쓰레기를 정리해주는 프로그램이다. 프로그램을 실행할 때 메모리를 관리하는 OS에 프로그램 실행에 필요한 메모리를 요청하게 되는데, 이 때 메모리를 어디에 저장할지 그 주소를 할당하는데 이 주소를 offset 주소라고 한다.

 

이 할당된 메모리 들은 프로그램이 돌아가면 필욘적으로 쓰레기가 발생한다. 기존에 가르키던 메모리를 새롭게 선언되거나 형변환이 되면서 다른 곳을 가리키게 되면서 주소를 잃어버리게 되고 다시 찾을 수 없게 되면서 정리되지 않은 메모리가 생겨버리게 되기 때문이다.

 

JVM은 메모리를 부여받고 프로그램을 실행하다가 메모리가 부족해지는 순간이 오면 추가적으로 메모리를 더 요청한다. 요청하는 바로 이때 GC가 실행된다.


GC에는 'stop-thie-world' 용어가 존재한다. 이 용어는 GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생한다. 대개의 경우 GC 튜닝이란 이 stop-the-world 시간을 줄이는 것이다.

 

Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터가 더 이상 필요 없는 객체를 찾아 지우는 작업을 한다. 이 가비지 컬렉터는 두 가지 가설 하에 만들어졌다.

 

  • 대부분의 객체는 금방 접근 불가능 상태가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

 

이러한 가설을 'weak generational hypothesis'라 한다. 이 가설의 장점을 최대한 살리기 위해서 HotSpot VM에서는 크게 2개로 물리적 공간을 나누었다. 이 공간을 Young 영역과 Old 영역이라고 한다.

 

- Young 영역

새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.

 

- Old 영역

접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.

 

영역별 데이터 흐름은 다음과 같다.

GC 영역 및 데이터 흐름도

 

여기서 Permanent Generation 영역은 Method Area라고 한다. 객체나 억류된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아 있는 곳은 절대 아니다. 이 영역에서 GC가 발생할 수도 있는데, 여기서 GC가 발행해도 Major GC의 횟수에 포함된다.

 

Old 영역에 있는 객체가 Young 객체를 참조하는 경우, 512 바이트의 덩어리로 되어있는 카드 테이블이 처리를 해준다. 카드 테이블에는 Old 영역에 있는 객체가 Young 영역을 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

 

카드 테이블 구조

 

카드 테이블은 write barrier를 사용하여 관리한다. write barrier는 Minor GC를 빠르게 할 수 있도록 하는 장치이다. write barrier 때문에 약간의 오버헤드는 발생하지만 전반적인 GC 시간은 줄어들게 된다.

 

 

 

2. Young 영역의 구성

객체가 제일 먼저 생성되는 Young 영역은 3개로 나뉜다.

 

  • Eden 영역
  • Survivor 영역(2개)

 

Survivor 영역은 2개이기 때문에 총 3개의 영역으로 나뉜다. 각 영역의 처리 절차는 다음과 같다.

 

  • 새로 생성한 대부분의 객체는 Eden 영역에 위치한다.
  • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
  • Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
  • 하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
  • 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.

 

즉, Survivor 영역 중 하나는 반드시 비어있는 상태로 남아 있어야 한다. 만약 2개의 Survivor 영역에 모두 데이터가 존재하거나 두 영역 모두 사용량이 0이라면 시스템은 정상적인 상황이 아니라고 생각하면 된다.

 

GC 전, 후

 

HotSpot VM에서는 보다 빠른 메모리 할당을 위해서 두 가지 기술을 사용한다. 하나는 bump-the-pointer라는 기술이며, 다른 하나는 TLABs (Thread-Local Allocation Buffers)라는 기술이다.

 

bump-the-pointer는 Eden 영역에 할당된 마지막 객체를 추적한다. 마지막 객체는 Eden 영역의 맨 위에 있다. 그리고 그 다음에 생성되는 객체가 있으면, 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인한다. 만약 해당 객체의 크기가 적당하다고 판정되면 Eden 영역에 넣게 되고, 새로 생성된 객체가 맨 위에 있게 된다. 따라서 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 메모리 할당이 이루어진다.

 

그러나 멀티 스레드 환경을 고려한다면 이야기가 달라진다. Thread-Safe 하기 위해서 만약 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 락이 발생할 수 밖에 없고, lock-contention 때문에 성능은 매우 떨어지게 될 것이다. HotSpot VM에서 이를 해결한 것이 TLABs이다.

 

각각의 스레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것이다. 각 쓰레드에는 자기가 갖고 있는 TLAB에만 접근할 수 있기 때문에, bumt-the-pointer 라는 기술을 사용하더라도 아무런 락이 없이 메모리 할당이 가능하다.

 

bump-the-pointer, TLABs 기술은 참고하는 용도로 기술했다. 결국 Young 영역에서 반드시 기억해야하는 중요한 포인트는, Eden 영역에서 최초로 객체가 만들어지고 Survivor 영역을 통해서 Old 영역으로 오래 살아남은 객체가 이동한다는 점이다.


- HotSpot VM

Hotspot JVM은 미국의 Longview Technologies LLC라는 회사에서 1999년에 처음 발표한 JVM이다. 이후 SUN에 인수되었으며, 1.2버전부터 SUN의 기본적인 JVM이 되었다. 현재 Hotspot JVM은 가장 일반적인 JVM 중 하나이다.

 

Hotspot은 말 그대로 Hot한 Spot을 찾아서 해당 부분에서는 JIT 컴파일러를 사용하는 방법이다. 내부적으로 프로파일링을 통해 핫스팟을 찾아내고, 해당 부분에 대한 네이티브 코드를 생성한다. 이때 네이티브 코드를 생성하는 방법에서 Client와 Server라는 두가지 방법이 존재한다.

 

- Thread-Safe

멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.


 

 

 

3. Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식에 따라서 처리 절차가 달라지기에 각 방식들에 대해 살펴보아야한다. GC 방식은 JDK 7 기준으로 5가지 방식이 존재한다.

 

  • SerialGC
  • Parallel GC
  • Parallel Old GC (Parallel Compacting GC)
  • Concurrent Mark & Sweep GC (이하 CMS)
  • G1 (Garbage First) GC

 

이 중에서 운영 서버에서 절대 사용하면 안되는 방식이 SerialGC이다. Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식이다. Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어진다.

 

- SerialGC

Old 영역의 GC는 Mark-Sweep-Compaction 알고리즘을 사용한다. 이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다. 그 다음에는 힙의 앞 부분부터 확인하여 살아 있는 것만 남긴다(Seep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction).

 

SerialGC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이다.

 

- Parallel GC

Parallel GC는 Serial GC와 기본적인 알고리즘은 같다. 그러나 Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 쓰레드가 여러 개이다. 따라서 Serial GC보다 빠르게 객체를 처리할 수 있다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Parallel GC는 Throughput GC라고도 부른다.

 

Serial GC vs Parallel GC

 

- Parallel Old GC

Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식이다. 앞서 설명한 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다. 이 방식은 Mark-Summary-Compaction 단계를 거친다. Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Mark-Sweep-Compaction 알고리즘의 Sweep 단계외 다르며 약간 더 복잡한 단계를 거친다.

 

- CMS

다음으로 CMS GC는 앞서 기술한 GC보다 훨씬 복잡하다.

Serial GC vs CMS GC

 

초기 Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다. 따라서 멈추는 시간은 매우 짧다 그리고 Concurrent Mark 단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다. 이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것이다.

 

그 다음 Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다. 마지막으로 Concurrent Sweep 단계에서는 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행한다.

 

이러한 단계로 진행되는 GC 방식이기 때문에 stop-the-world 시간이 매우 짧다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며 Low Latency GC라고도 불린다.

 

하지만 CMS GC는 stop-the-world 시간이 짧다는 장점에 비해 단점도 존재한다.

 

  • 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
  • Compaction 단계가 기본적으로 제공되지 않는다.

 

따라서 CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 한다. 그리고 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 더 길기 때문에 Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.

 

- G1 GC

G1 GC는 앞서 설명된 Young 영역과 Old 영역에 대해 생각하지 않는 편이 좋다.

 

G1 GC는 바둑판의 각 영역에 각 객체를 할당하고 GC를 실행한다. 그러다 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다. 즉, 지금까지 설명한 Young의 세 가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이다. G1 GC는 장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해 만들어졌다.

 

G1 GC의 가장 큰 장점은 성능이다.

GC의 레이아웃

 

 

 

4. GC 모니터링

GC 모니터링은 JVM이 어떻게 GC를 수행하고 있는지 알아내는 과정이다. 가령 Young 영역에서 Old 영역으로 언제 얼마나 이동했는지, stop-the-world가 언제 일어나고 얼마동안 일어났는지 등의 정보를 알 수 있다. 이를 통해 JVM이 효율적으로 GC를 수행하는지 파악하고 추가적인 GC 튜닝 작업이 필요한지 확인하기 위해서이다.

 

GC를 모니터링하는 방법으로는 여러가지가 있지만, GC 수행 정보를 사용자에게 보여주는 방법만 다르다. GC는 JVM이 수행하는 것이고, GC 모니터링 도구는 JVM이 제공하는 GC 정보를 받아 사용자에게 보여주는 것이기 때문에 어떤 방법을 해도 동일한 결과를 출력해낸다. 따라서 GC 모니터링 방법을 모두 익힐 필요없다.

 

HotSpot JVM을 기준

GC 모니터링 방법은 접근 인터페이스에 따라 CUI와 GUI로 구분된다.

 

대표적은 CUI GC 모니터링 방법에는 'jstat'이라는 CUI 애플리케이션을 이용하는 방법과 JVM을 가동할 때 '-verbosegc'라는 JVM 옵션을 이용하는 방법이 있다.

 

GUI GC 모니터링 방법은 별도의 GUI 애플리케이션을 이용한다 대표적으로는 'jconsole, jvisulvm, Visual GC'가 있다.

 

- jstat

jstat은 HotSpot JVM에 있는 모니터링 도구이다. jstat 이외에 HotSpot JVM 모니터링 도구로는 jps와 jstatd가 있다. Java 애플리케이션을 모니터링할 때에는 이 세 개의 도구를 모두 사용해야할 경우도 있다.

 

jstat은 GC 수행 정보를 보는 기능만 제공하는 것이 아닌, 클래스로더 수행 정보나 Just-In-Time 컴파일러 수행 정보 등도 jstat으로 알 수 있다. jstat은 $JDK_HOME/bin 디렉터리에 있다. 커맨드 라인에서 디렉터리를 지정하지 않고 바로 java, javac 명령어를 실행할 수 있다면, jstat도 바로 실행할 수 있다.

$ jstat stat , SIC, SOU

S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT  
3008.0 3072.0 0.0 1511.1 343360.0 46383.0 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588  
3008.0 3072.0 0.0 1511.1 343360.0 47530.9 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588  
3008.0 3072.0 0.0 1511.1 343360.0 47793.0 699072.0 283690.2 75392.0 41064.3 2540 18.454 4 1.133 19.588  

 

vmid는 VM을 가르키는 ID이다. 로컬 머신에서 동작하는 Java 애플리케이션만이 아닌 리모트 머신에서 동작하는 Java 애플리케이션도 vmid로 가리킬 수 있다. 로컬 머신에서 동작하는 Java 애플리케이션에 대한 vmid를 lvmid(Local vmid)라 하는데, 많은 경우 이 lvmid는 PID이다. 따라서 ps 명령이나 Windows 작업 관리자를 이용하여 확인한 PID 값을 lvmid로 쓸 수 있지만, PID와 lvmid가 언제나 일치하는 것이 아니기 때문에 jps를 이용하는 것이 좋다.

 

jps는 Java PS로, ps가 PID와 프로세스명을 알려주듯이 vmid와 main 메서드 정보를 보여준다. jps를 이용하면 모니터링하려는 Java 애플리케이션의 vmid를 알아낸 후 jstat의 인자로 사용한다. jps만 사용하면 WAS 인스턴스가 한 장비에서 여러 개 실행되고 있을 때 모두 부트스트랩 정보만 나타내는 단점이 있기 때문에 'ps -ef | grep java' 명령을 함께 사용하는 것도 좋다.

 

GC 수행 정보는 지속적으로 관찰해아하므로, 일정 시간마다 계속 GC 모니터링 정보를 출력하도록 jstat을 실행할 수 있다.

// 1초마다 GC 모니털이 정보를 콘솔에 출력
$ jstat -gc <vmid> 1s

 

gc 명령어 외에도 다양한 옵션이 존재하는데, 사용 빈도를 따지면 'gcutil (또는 -gccause), -gc, -gccapacity'의 순서로 많이 사용한다. '-gcutil' 옵션으로 힙 영역의 사용 정도와 GC 발생 횟수, 실행 누적 시간을 확인하고 '-gccapacity' 옵션 등을 이용하여 실제 할당 크기를 알 수 있기 때문이다.

 

jstat 옵션에 따라 출력되는 칼럼 종류가 다양하다. 그 중 GC 튜닝 시 유념해서 봐야하는 항목은 YGC, YGCT, FGC, FGCT, GCT이다. 이 항목들을 통해 GC를 수행하는데 시간이 얼마나 소요되었는지 알 수 있기 때문이다. 하지만 산술 평균을 통해 시간 소요를 확인하기에는 편차가 심할 수 있기 때문에 개별적인 GC 시간을 파악하기 위해 '-verbosegc' 옵션을 이용하는 것이 더 좋다.

 

- -verbosegc

이 옵션은 Java 애플리케이션을 가동할 때 지정하는 JVM 옵션 중 하나이다. jstat은 특별한 옵션을 지정하지 않은 어떤 Java 애플리케이션도 모니터링할 수 있는 반면 '-verbosegc' 옵션은 시작할 때 지정해야 하기 때문에 굳이 사용할 필요가 없는 옵션으로 볼 수 있다. 하지만 직관적으로 이해하기 쉬운 출력 결과를 GC가 발생할 때마다 보여 주기 때문에 개략적인 GC 정보를 모니터링할 때에는 좋다.

 

 

 

5. GC Collection 튜닝

모든 Java 기반의 서비스에서 GC 튜닝을 진행할 필요는 없다. GC 튜닝이 필요 없다는 이야기는 운영 중인 Java 기반 시스템의 옵션과 동작이 다음과 같다는 의미이다.

 

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

 

즉, 메모리 크기도 지정하지 않고 Timeout 로그가 수도 없이 출력되는 경우 GC 튜닝을 하는 것이 좋다. 단, GC 튜닝은 가장 마지막에 하는 작업임을 기억해야한다.

 

GC는 생성된 객체가 많으면 처리해야하는 대상이 많아지고 횟수도 증가한다. 따라서 GC를 적게 하기 위해서는 객체 생성을 줄이는 작업을 수행해야한다. 가령 String 대신 StringBuilder, StringBuffer를 사용하는 것을 생활화하는 것이 시작이다. 또한 로그를 최대한 적게 쌓도록 하는 것이 좋다.

 

GC 튜닝은 Old 영역으로 넘어가는 객체의 수를 최소화하는 것과 Full GC의 실행 시간을 줄이는 두 가지 목적으로 나눌 수 있다.

 

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

JDK 7 부터 본격적으로 사용이 가능한 G1 GC를 제외한 모든 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 실행에 시간이 오래 소요되면 연계된 여러 부분에서 타임아웃이 발생할 수 있다. 그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 자칫 OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어난다. 반대로 Old 영역의 크기를 늘리면 Full GC 횟수는 줄어들지만 실행 시간이 늘어난다. Old 영역의 크기를 적절하게 잘 설정해야한다.

 

튜닝 시 중요한 것은 각 서비스마다 생성되는 객체의 크기도 다르고 살아있는 기간도 다르기 때문에 타 서비스와 동일하게 옵션을 사용해서는 안된다. 다양한 옵션을 많이 설정한다고 해서 시스템의 GC 수행 속도가 월등히 빨라지진 않는다. 오히려 더 느려질 확률이 높다. 두 대 이상의 서버에 GC 옵션을 다르게 적용해서 비교해 보고, 옵션을 추가한 서버의 성능이나 GC 시간이 개선된 때에만 옵션을 추가하는 것이 GC 튜닝의 기본 원칙이다.

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

가장 많이 사용하는 옵션으로는 -Xms, -Xmx, -XX:NewRatio 옵션이다. 특히 -Xms, -Xmx 옵션은 필수로 지정해야 하는 옵션이다. 또한 NewRatio 옵션을 어떻게 설정하느냐에 따라 GC 성능에 많은 차이가 발생한다.

 

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

 

GC의 성능에 많은 영향을 주는 또 다른 옵션은 GC 방식이다. (JDK 6 기준)

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

G1 GC를 제외하고 각 GC 방식의 첫 번째 줄에 있는 옵션을 지정하면 GC 방식이 변경된다. GC 방식 중에서 특별시 신경쓸 필요가 없는 방식은 Serial GC다. 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가 발생한다면, 힙 덤프를 떠서 그 원인을 확인하고 문제점을 제거해야한다.

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

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

4. 결과 분석

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

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

 

 

 

6. 요약

- Stop-the-world

GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업이 완료한 이후에는 작업을 다시 시작하며, 어떤 GC 알고리즘을 사용하더라도 발생한다. GC 튜닝이란 이 stop-the-world 시간을 줄이는 것을 의미한다.

 

- Young 영역

새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.

 

Young 영역에는 Eden과 두 개의 Survivor 영역으로 나뉜다. Eden 영역에서 GC가 일어나면 Survivor 영역으로 이동하고, 지속적으로 해당 Survivor 영역에 이동이 되어 가득 찰 경우 다른 Survivor 영역으로 이동시키는 과정을 거친다. 이 과정을 반복하면서 계속해서 살아있는 객체를 Old 영역으로 이동된다.

 

즉, Eden 영역에서 최초로 객체가 만들어지고, Survivor 영역을 통해 Old 영역으로 오래 살아남은 객체가 이동한다.

 

 

- Old 영역

접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.

 

Old 영역에 대한 GC는 JDK 7 기준으로 5가지가 존재한다.

  • Serial GC - 적은 메모리, CPU 코어 개수가 적을 때 사용
  • Parallel GC - 메모리가 충분하고 코어의 개수가 많을 때 사용
  • Parallel Old GC
  • CMS - Stop-the-world 시간이 매우 짧고 애플리케이션의 응답 속도가 매우 중요할 때 사용 (단, Compaction 작업이 얼마나 자주 수행되는지 확인 필요)
  • G1 GC - 앞의 모든 GC보다 성능이 뛰어남

 

- 카드 테이블

Old 영역에 있는 객체가 Young 객체를 참조하는 경우 카드 테이블이 처리를 해준다. 카드 테이블에는 Old 영역에 있는 객체가 Young 영역을 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다. write barrier를 사용하여 관리하며, write barrier는 Minor GC를 빠르게 할 수 있도록 도와준다.

 

- jstat을 이용한 모니터링

 

- GC 튜닝

  • Old 영역으로 넘어가는 객체의 수 최소화하기
  • Full GC 시간 줄이기
  • 모니터링을 통해 GC 튜닝 여부를 결정하고, GC 방식과 메모리 크기를 여러 대의 서버에 옵션을 서로 다르게 지정하고 차이를 확인 후 가장 알맞는 옵션을 적용

[참고] d2.naver.com/helloworld/1329

[참고] d2.naver.com/helloworld/6043

[참고] d2.naver.com/helloworld/37111

728x90

'Language > Java' 카테고리의 다른 글

[Java] JVM, JDK (JRE), JAR vs WAR  (0) 2021.04.22
[Java] LocalDate, LocalTime, LocalDateTime  (0) 2021.04.03
[Java] 객체지향 5대 원칙 SOLID  (0) 2020.11.02
[Java] 명명 규칙  (0) 2020.07.21