Java

[Java] Garbage Collection(4) - Young Generational GC, Old Generational GC

재담 2022. 4. 14. 00:13

Young Generational GC

이번에는 Eden 영역에 대해 더 알아보자. 참고로 HotSpot VM에서는 보다 빠른 메모리 할당을 위해서 2가지 기술을 사용한다. 첫 번째는 bump-the-pointer라는 기술이고, 두 번째는 TLABs(Thread-Local Allocation Buffers)라는 기술이다.

 

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

 

그러나 멀티 스레드 환경에서는 Thread-Safe 하기 위해서 Eden 영역에 저장할 때 락(lock)이 발생할 수밖에 없고, Lock Contention 때문에 성능이 매우 떨어지게 된다.

 

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

 

 

Old Generational GC

Old 영역에서는 기본적으로 데이터가 가득 차면 GC를 수행한다. Old 영역에서의 GC 방식에 따라 절차가 달라지는데, 그 종류를 살펴보자. 지금부터 설명하는 내용은 JDK 7을 기준으로 설명한다.

 

Serial GC (-XX:+UseSerialGC)

Serial GCMark-Sweep-Compact 알고리즘을 사용한다. 이 알고리즘은 다음과 같은 순서로 실행된다.

  1. Old 영역에 살아 있는 객체를 식별(Mark)한다.
  2. 힙의 앞부분부터 확인하여 살아 있는 것만 남긴다.(Sweep)
  3. 각 객체가 연속되게 쌓이도록 힙의 가장 앞부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다.(Compaction)

 

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

 

Parallel GC (-XX:UseParallelGC)

Parallel GC는 Serial GC와 기본적인 알고리즘은 같다. Minor GC, Full GC 모두 All Stop인 건 동일한데, Serial GC는 GC를 처리하는 스레드가 1개인 것에 비해 Parallel GC는 스레드가 여러 개다. 그렇기 때문에 이름이 Parallel이다. 멀티 스레드이기 때문에 Serial GC보다 빠르게 객체를 처리할 수 있다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Parallel GC는 Throughput GC라고도 부른다. 다음 그림은 Serial GC와 Parallel GC의 스레드를 비교한 그림이다.

출처 : https://goodgid.github.io/Java-Garbage-Collection-(2)/

 

CMS(Concurrent Mark & Sweep) GC (-XX:UseConcMarkSweepGC)

CMS GCFull GC의 stop-the-world 상태를 어떻게 줄일 수 있을까라는 고민에서 출발했다. Parallel GC와 마찬가지로 멀티 스레드로 Minor GC를 한다. 그리고 이 순간에는 stop-the-world가 발생한다. 하지만 Full GC는 stop-the-world가 거의 발생하지 않는다. 애플리케이션이 작동하는 중에 백그라운드에서 스레드를 만들어 Old Generation 영역에 참조되지 않은 객체들을 지속해서 제거한다. 다음 그림은 Serial GC와 CMS GC를 비교한 그림이다.

출처 : https://goodgid.github.io/Java-Garbage-Collection-(2)/

 

위 그림에서 CMS GC는 다음과 같은 방식으로 실행된다.

  1. Initial Mark : 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다. 따라서 멈추는 시간이 매우 짧다.
  2. Concurrent Mark : 첫 단계에서 살아 있다고 확인한 객체가 참조하고 있는 객체들을 따라가면서 확인한다. 이 단계는 다른 스레드가 실행 중인 상태에서 동시에 진행된다.
  3. Remark : 두 번째 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  4. Concurrent Sweep : 쓰레기를 정리하는 작업을 실행한다. 이 단계도 다른 스레드와 동시에 진행된다.

 

이러한 단계로 진행되어 stop-the-world 시간이 매우 짧다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며 Low Latency GC라고도 부른다. 하지만 다음과 같은 단점도 존재하므로 신중히 검토 후 사용해야 한다.

  • 백그라운드에서 항상 GC 스레드가 돌아야 하므로 다른 GC 방식보다 메모리와 CPU를 많이 사용한다.
  • Compaction(압축) 단계가 기본적으로 제공되지 않으므로 메모리 파편화가 발생한다.
  • CPU 리소스가 부족해지거나 메모리 파편화로 인해 메모리 공간이 부족해지면 Serial GC 방식(싱글 스레드)을 똑같이 따라 하게 된다.

 

G1(Garbage First) GC (-XX:UseG1GC)

G1 GC는 장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해서 만들어졌다. G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다. G1 GC는 아래 그림과 같이 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다 해당 영역이 가득 차면 다른 영역에 객체를 할당하고 GC를 실행한다. 즉 지금까지 설명한 Young의 3가지 영역에서 객체가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다.

출처 : https://goodgid.github.io/Java-Garbage-Collection-(2)/

 

G1 GC의 가증 큰 장점은 성능이다. 지금까지 설명한 어떤 GC 방식보다도 빠르다. JDK 7에서 정식으로 G1 GC를 포함하여 제공한다. G1 GC는 힙 영역이 매우 큰 머신(최소 4GB)에서 돌리기에 적합한 GC이다.

 

힙에 영역(Region)이라는 개념을 도입하였는데, 힙을 여러 개의 Region으로 나눠 일부 Region은 Young Generation 영역으로 사용하고 나머지 일부 Region은 Old Generation 영역으로 사용한다. Young Generation 영역을 정리하는 건 Parallel GC나 CMS GC처럼 멀티 스레드로 정리한다. 그리고 Old Generation 영역에 해당하는 Region에 대해선 CMS GC처럼 백그라운드 스레드로 정리한다. CMS GC와의 차이점은 중간중간 쓸모없는 객체들을 정리하는 것이 아닌 한 Region을 통째로 정리한다. 참조가 없는 객체들은 지우고 사용 중인 객체들은 다른 Region으로 고스란히 복사한다(객체 이동 기법). 다른 Region으로 복사하는 과정에서 Compacting이 되므로 메모리 파편화 현상이 생기지 않게 된다. CMS GC의 단점이었던 메모리 파편화 문제를 해결하게 된 것이다.


Reference