이번엔 저번에 공부했던 JVM에 이어 GC에 대해 알아보자.

GC?

GCJVM 내에 존재하는 하나의 부속 머신이다.
Garbage Collector의 약어로, 말 그대로 쓰레기 청소부 역할을 한다.
Java의 가장 큰 특징인 자동화된 메모리 관리의 핵심이다.

사용되지 않는 메모리에 대해 누수가 발생하지 않기 위해 도와주는 것이다!

GC 대상

먼저 GC의 대상은 기본적으로 동적 메모리인 Heap 공간이다.
이전에 알아보았듯이, Runtime Data Area 내에는 Heap이 있다.
해당 공간에 동적으로 할당된 메모리들을 자동으로 해제하는 것이다.

해당 공간은 자세하게 아래와 같이 분할할 수 있다.
image

  • Young generation
    • Eden : 생성된지 얼마 되지 않은 객체들
    • Survivor : 최소 1번 GC가 실행되어 Eden에서 옮겨온 객체들
      • 둘 중 하나는 반드시 비어있어야 한다는 규칙이 있다.
  • Old generation
    • Old : 생성된지 오래되어 Old로 옮겨진 객체들
  • Permanent generation
    • Permanent : 클래스의 메타데이터가 저장되는 공간
    • Java 8 버전 이후로는 Native method stack으로 옮겨갔다고 한다.

위의 구조를 보았을 때, YoungOld 모두 GC의 대상이다.
이 때 Young을 대상으로 진행하는 CollectionMinor GC라고 한다.
또, Old를 대상으로 진행하는 CollectionMajor GC라고 한다.

Minor GC

기본적으로 Collection 동작은 Mark and Sweep 방식으로 동작한다.
아래와 같은 순서로 Minor GC가 진행된다.

  1. 만약 Eden이 꽉찼다면? Minor GC가 수행된다.
  2. Reachable, 즉 아직 사용중인 객체에 Mark를 한다.
  3. Mark된 객체들을 Survivor 공간으로 옮긴다.
  4. 남은 Unreachable 객체들을 Sweep한다.
  5. 이 때 살아남은 객체들은 Survivor에 모여있으며, Age를 하나 증가시킨다.
  6. 이후 흩어진 객체들을 Heap의 시작주소로 모아 Compact 시킨다.

JVM의 GC에는 Age 개념이 존재한다.
Age가 임계값을 넘어가는 경우, Old 공간으로 옮기게 되는 것이다.
보통은 6bit 값으로 표현되기 때문에 31을 임계값으로 두고 있다고 한다.

Major GC

이번에는 Old 영역이 가득차면 발생하는 Major GC에 대해 알아보자.
Major GC는 흔히 얘기하는 큰 오버헤드의 주범 Full GC와 같은 말이다.
단순히 Old의 모든 객체를 싹 검사하여 Unreachable 객체들을 버린다.
삭제하고, 앞서 Minor GC와 같이 Compact 과정을 진행한다.

하지만 Old 영역은 Young 영역에 비해 매우 크다.
그렇기에, Young 영역을 탐색하는 시간에 비해 훨씬 많은 시간이 걸린다.
우리는 이 시간을 Stop the world라고 칭한다.
많은 개발자들이 이 오버헤드를 줄이기 위해 많은 연구를 진행했다.

GC 알고리즘

Serial GC

가장 단순한 방식의 GC이다.
그냥 하나의 스레드가 GC를 진행하게 되며, STW가 매우 길다고 볼 수 있다.

Young 영역은 Mark-Sweep을 사용한다.
Old 영역은 Mark-Sweep-Compact를 사용한다.

Parallel GC

Java 8에서 기본적으로 사용하는 GC 방식이라고 한다.
Serial GC에서 개선하여, Minor GC를 멀티스레드 방식으로 수행한다.

당연히 멀티스레드 방식이기에 STW가 조금은 줄어든다.

Parallel Old GC

Old 영역까지 멀티스레드 방식으로 처리하는 개선된 방식이다.
여기서는 Mark-Summary-Compact라는 새로운 알고리즘이 도입된다.

STWMajor GC에서 크게 발생하는데, 이를 크게 줄일 수 있을 것이다.

Concurrent Mark Sweep

Application 스레드와 GC 스레드를 아예 동시에 실행하는 방식이다.
STW 시간을 최대한 줄일 수 있으나, GC 과정이 매우 복잡해졌다.

GC에 대한 CPU 사용량도 높아지고, 메모리 파편화도 발생해 사용이 중지되었다.

G1 GC

Garbage First GC라고도 불리는 알고리즘이다.
CMS GC를 대체하여 사용하기 위해 고안되었으며, Java 9+의 기본 GC이다.

여기서부터는 Heap 공간의 구조가 아예 바뀌어버린다.
보통은 Eden, Survivor, Old로 공간을 크게 3군데로 나누었었다.
하지만 여기서는 공간을 Region이라는 개념으로 잘라 역할을 동적으로 부여한다.

메모리 전체를 탐색하지 않고, 메모리가 많이 찬 Region 단위로 탐색을 수행한다.
그렇기에 기존의 GC 방식보다는 STW가 꽤 줄었다고 볼 수 있다.
4GB 이상의 Heap 공간과, 0.5초 정도의 STW를 원할 때 사용하길 권장한다고 한다.

Shenandoah GC

Redhat에서 개발한 GC 방식이다.
CMS GC의 단편화를 해결하고, G1의 Pause 이슈를 해결한 방식이라고 한다.

ZGC

JDK 15와 17에서 GC로 인정받아 사용되는 최신 방식의 GC라고 한다.
대량의 메모리를 유연하게 처리하기 위해 고안된 방식이다.
G1 GCRegion 개념과 같이 ZPage 공간을 사용한다.
Region과 다르게 고정 공간이 아닌, 2^N으로 동적 공간을 할당한다.

Heap의 크기가 증가하더라도 STW10ms를 넘지 않는다는 것이 큰 장점!

보통 GC 튜닝을 한다고 하면, 위의 알고리즘들을 고르는 것을 의미한다.
현재 환경에서 과연 어떤 알고리즘이 최적일지 성능을 비교하게 되는 것이다.

GC의 단점

이렇게 엄청 편리할 것 같은 GC에도 몇 가지 단점이 있다.

  1. 당연하게도 STW로 인한 오버헤드가 단점일 수 있겠다.
  2. GC가 실행되는 시점이 언제인지 알 수가 없다.
    • 즉, 개발자가 메모리 해제 시점을 예측 할 수 없다!