[DevOps] 1편 : Garbage Collector 동작 원리의 이해

안녕하세요? 정리하는 개발자 워니즈입니다. 이번시간에는 garbage collector 에 대해서 정리해보는 시간을 갖도록 하겠습니다. 이전에 어플리케이션을 구동할 때 별다른 옵션없이 기본값을 사용해 왔었습니다. 하지만, 최근에 어플리케이션 운영을 하면서 모니터링. 지표상 메모리가 지속적으로 증가하는 현상을 식별했습니다.

JVM의 초기 힙메모리 사이즈 지정이 의심이 가게 됐고, Garbage Collector의 동작 방식 그리고 어떤식으로 튜닝을 진행하는지에 대해서 정리하기 위해 이 포스팅을 작성합니다.

gc_1

1. Garbage Collection 과정

먼저, 한번쯤은 stop the world에 대해서 들어봤을 것입니다. 이는 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈추는 현상을 말합니다. 어떤 GC 알고리즘이라도 stop the world는 발생합니다. 대게의 경우 이러한 현상을 줄이는 것이 GC 튜닝의 목적입니다.

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

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

VM에서는 공간을 2개로 나누고 있습니다.

  • Young 영역 : 새롭게 생성한 객체의 대부분이 여기에 위치합니다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라집니다. (Minor GC)
  • Old 영역 : 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사됩니다. 대부분은 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생합니다. (Major GC)

2. Young 영역의 구성

Young 영역은 3개의 영역으로 나뉩니다.

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

순서는 다음과 같습니다.

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

Eden 영역에 최초로 객체가 만들어지고, Survivor 영역을 통해서 Old 영역으로 오래 살아남은 객체가 이동한다는 원리를 이해하면 됩니다.

3. Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행합니다. GC방식은 총 5가지가 존재하며 다음과 같이 간단하게 소개하겠습니다.

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC(CMS)
  • G1(Garbage First) GC

이 중에서 운영 서버에서 절대 사용하면 안되는 방식은 Serial GC라고 합니다. 이는 PC의 CPU 코어가 하나만 있을 때 사용하기 위해 만든 방식이라고 합니다.

그러면 하나씩 살펴보도록 하겠습니다.

3-1. Serial GC (-XX:+UseSerialGC)

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

3-2. Parallel GC (-XX:+UseParallelGC)

Parallel GC는 Serial GC와 기본적인 알고리즘은 같습니다. 그러나, 여러개의 쓰레드를 통해 처리하기 떄문에 Serial GC보다는 당연히 빠르게 처리합니다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리 합니다.

3-3. Parallel Old GC (-XX:+UseParallelOldGC)

Parallel Old GC는 앞의 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다릅니다. 이 방식은 Mark-Summary-Compaction 단계를 거칩니다. Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아있는 객체를 식별한다는 점에서 Sweep 단계와 다르며, 약간 더 복잡한 단계를 거칩니다.

3-4. CMS GC(-XX:+UseConcMarkSweepGC)

초기 Initial Mark단계 : 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝냄.
Concurrent Mark단계 : 살아 있따고 확인한 객체에서 참조하고 있는 객체들을 따라가며 확인(동시 진행)
Remark 단계 : Concurrent 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.

위의 단계로 진행이 되다보니, stop-the-world 시간이 매우 짧습니다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, Low Latency GC라고도 부릅니다.

단점

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

3-5. G1 GC

G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행합니다. Young, Old영역으로 이동하는 단계각 사라진 GC방식이라고 이해하면 됩니다. G1 GC는 장기적으로 문제가 많은 CMS GC를 대체하기 위해서 만들어졌습니다.

G1 GC의 가장 큰 장점은 성능입니다. 지금까지 설명한 어떤 GC방식보다도 빠릅니다.

4. 마치며…

이번 포스팅을 통해서 간단하게 GC가 어떤방식으로 동작하는지를 확인했습니다. 다음 포스팅은 실제 메모리 누수를 확인하고 해당 부분을 GC 로그 분석을 통해서 어떤부분에서 누수가 발생을 했고 해결은 어떤식으로 했는지를 작성해보겠습니다.

예전에는 GC 튜닝과 같은 작업은 엄청 어려운 영역이라고 생각했는데 필자가 생각하는 튜닝의 기본은 사실 디폴트로 제공해주는 것을 사용하고 문제가 발생했을 때 그 원인을 파악하는게 맞다고 생각하고 있습니다. (기본을 사용하는 이유는 다 있을것이라고 생각이 듭니다.)

하지만 개별적인 어플리케이션 마다의 상황이 있기 때문에 튜닝 필요시 적절한 값을 찾아나가는 것도 필요하긴 한 거 같습니다.

5. 참고

Java Garbage Collection

Garbage Collection 튜닝

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다