1. Java Garbage Collection이란?

 

애플리케이션을 개발하면, 유효하지 않은 메모리 즉 필요 없는 Garbage가 발생합니다. Java는 JVM의 Garbage Collector(GC)는 임의로 불필요한 메모리를 정리해줍니다. 간단한 코드를 보겠습니다. 

 

Animal이란 인터페이스를 상속받은 Dog과 Cat이 있습니다.

처음 animal 객체는 Dog을 구현체로 가집니다. 하지만 null로 참조를 해제함으로써 유요 하지 않은 Garbage가 됩니다. 이러한 것을 GC가 메모리를 청소해줍니다. 하지만 정확한 시점은 개발자가 알 수 없습니다. 

Animal animal = new Dog();

// garbage 발생
animal = null;

animal = new Cat();

 

 

물론 사용자가 System.gc() 호출하여 할 수는 있지만 'stop-the-world' 때문에 애플리케이션에 성능에 막대한 영향을 끼칠 수 있습니다.

여기서 'stop-the-world'란? GC를 위해 JVM이 일시적으로 GC를 실행하는 스레드를 제외한 모든 스레드 작업을 멈춥니다. 따라서 System.gc()는 호출하면 안 됩니다. 

 

2. Garbage Collection 과정

 

GC는 두 가지 가설하에 만들어졌습니다.

1. 대부분의 객체는 금방 접근 불가능 상태(unreachgable)가 된다.

2. 오래된 객체에서 젊은 객체로의 잠조는 아주 적게 존재한다.

 

위의 두 가설을 합쳐 'weak generational hypothesis'라고 칭합니다. 이 가설들의 장점을 극대화하기 위해 HotSpot VM에서는 2개의 물리 공간으로 나눴습니다. Young, Old입니다. 

1. Young

- 새롭게 생성한 객체의 대부분이 위치하는 곳

- 대부분의 객체는 금방 접근 불가능 상태가 되므로 많은 객체가 이 영역에 생성됐다가 사라집니다. 

- 객체가 사라질 때 Minor GC가 발생했다고 말합니다.

 

2. Old

- 접근 불가능 상태로 되지 않아 Young에서 살아남은 객체가 복사됩니다.

- 보통 Young보다 큰 크기로 할당됩니다.

- Young보다 적게 GC가 발생합니다.

- Old에서 객체가 사라질 때는 Major GC가 발생했다고 말합니다.

 

2가지 가설중에  "오래된 객체에서 젊은 객체로의 잠 조는 아주 적게 존재한다."의 가설에 위배하는 예외가 있을 수 있습니다. 이때는 Old 영역에 존재하는 512 바이트 덩어리(chunck)로 되어 있는 card table을 이용합니다. 

카드 테이블에서는 Old에 있는 객체가 Young에 있는 객체를 참조할 때마다 정보가 표시됩니다. Young에 GC를 실행할 때 Old 영역에 있는 모든 객체를 확인하지 않고, 카드 테이블만 확인하여 GC 대상인지 판별합니다. 

카드 테이블 구조

 

3. Young 영역의 GC (Minor GC)

 

Young영역의 GC를 이해하기 위해선 구성을 먼저 알아야 합니다. Young 영역은 총 3개의 영역으로 나뉩니다.

 

1. Eden

2. Survivor (2개)

 

GC 순서도는 아래와 같습니다. 

1. 새로 생성한 대부분의 객체는 Eden에 위치합니다.

2. Eden에서 GC가 실행된 후 살아남은 객체는 Survivor 영역으로 이동합니다.

3. 2번 과정에서 계속해서 살아남은 객체들이 Survivor 영역에 쌓이게 됩니다. 

4. 하나의 Survivor가 가득 차게 되면 그중에서 살아남은 객체들을 다른 Survivor로 모두 이동시킵니다. 그렇다면 가득 찼던 Survivor 영역은 빈 상태로 바뀌게 됩니다.

5. 2~4과정에서 계속 살아남은 객체는 Old 영역으로 이동합니다.

Minor GC 전과 후 비교 사진

HotSpot VM에서는 보다 빠른 메모리를 할당하기 위해 2가지 기술을 사용합니다.

 

1. bump-the-pointer

2. TLABs(Thread-Local Allocation Buffers)

 

하나씩 알아보겠습니다.

 

bump-the-pointer

 

Eden 영역에서 마지막에 할당된 객체를 추적합니다. 이 마지막 객체는 Eden 영역의 맨 위에 위치합니다. 따라서 다음 생성되는 객체가 존재한다면, 해당 객체의 크기가 Eden에 넣을 수 있는 크기인지 확인합니다. 이 방법을 통해 마지막에 추가된 객체만 점검하면 되므로 빠른 메모리 할당이 가능해집니다. 

 

TLABs

 

그러나 위의 과정이 멀티 스레드 환경이라면 상황이 다릅니다. Thread-safe 하기 위해서 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하기 위해선 Lock이 발생하고, 이로 인해 성능이 떨어닙니다. 그래서 HotSpot VM에선 TLABs를 사용합니다.

이것은 각각의 스레드가 자신의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하여, bump-the-pointer를 사용해도, Lock 상황 없이 메모리 할당이 가능합니다. 

 

4. Old 영역의 GC (Major GC)

 

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행합니다. GC 방식에 따라서 처리 절차가 달라집니다. 총 6가지 방식의 GC에 대해서 알아보겠습니다. 

 

1. Serial GC

2. Parallel GC

3. Parallel Old GC(Parallel Compacting GC)

4. Concurrent Mark & Sweep GC(이하 CMS)

5. G1(Garbage First) GC

6. ZGC

 

4-1. Serial GC(적은 메모리 적은 CPU 코어에 적합)

 

Young 영역에서의 GC는 위에 설명한 방식으로 진행됩니다. Old 영역의 GC는 mark-sweep-compact 알고리즘을 사용합니다. 해당 알고리즘은 Old 영역에 살아 있는 객체를 식별(Mark)합니다. 그 후 힙의 앞부분부터 확인해, 살아있는 것만 남깁니다.(Sweep). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눕니다.(Compaction).

 

 

 

4-2. Parallel GC(메모리가 충분, 코어의 수가 많을 때 유리)

 

위에서 본 Serial GC와 기본적으로 같은 알고리즘입니다. 하지만 Serial GC는 GC를 실행하는 쓰레드가 하나지만, Parallel GC는 GC를 실행하는 스레드가 여러 개입니다. 따라서 더 빠른 성능을 가집니다. 

GC 비교사진

 

4-3. Parallel Old GC

 

Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식입니다. 앞서 살펴본 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다릅니다. 이 방식은 Mark-Summary-Compaction 단계를 사용합니다. Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Mark-Sweep-Compaction 알고리즘의 Sweep 단계와 다르며, Old GC 처리량을 늘려주기 위한 작업입니다. 

 

4-4. CMS GC

 

CMS GC는 위에서 봐왔던 GC들보다 복잡합니다.

 

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

 

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

 

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

 

 그런데 CMS GC는 stop-the-world 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재합니다.

 

1. 다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.

2. Compaction 단계가 기본적으로 제공되지 않는다.

 

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

SerialGC와 CMS GC

 

4-5. G1 GC

 

G1 GC는 큰 힙 메모리에서 짧은 GC 시간을 보장하는 것을 목적으로 합니다. 위에서 살펴본 GC들과 다르게 힙 메모리를 관리합니다. Eden, Survivor, Old 영역이 존재하지만, 고정적인 크기가 아닙니다. 전체 힙 메모리 영역을 Region이라는 크기로 나눠 각각 동적으로 할당합니다.

 

JVM의 힙은 2048개의 Region으로 나뉠 수 있으며, 각 Region의 크기는 1MB~32MB로 설정할 수 있습니다.

 

G1 GC 형태

 

G1 GC는 위에서 살펴본 GC와 다르게 Heap영역에서 Humongous와 Available/Unuse가  존재합니다.

 

1. Humongous -> Region 크기의 50%를 초과하는 큰 객체를 저장하기 위한 공간이며, GC 최적으로 동작 X

2. Available/Unuse -> 아직 사용되지 않은 Region

 

 

Young GC

 

각 Region 중 GC 대상 객체가 많은 Region(Eden, Survivor)에서 수행됩니다. Region에서 살아남은 객체는 다른 Region(Survivor)로 옮깁니다. 그리고 비워진 Region을 가용상태로 돌립니다. 

 

Old GC

 

G1 GC에서 Full GC 가 수행될 때는 Initial Mark -> Root Region Scan -> Concurrent Mark -> Remark -> Cleanup -> Copy 단계를 거치게 됩니다.

 

initial Mark -> Old Region에 존재하는 객체들이 참조하는 Survivor Region을 찾습니다. 이때 stop-the-world가 발생

 

Root Region Scan -> Initial Mark에서 찾은 Survivor Region에 대한 GC 대상 객체 스캔 작업을 진행

 

Concurrent Mark -> 전체 힙의 Region에 대해 스캔 작업 진행하고, GC 대상 객체가 발견되지 않은 Region은 이후 단계 제외

 

Remark -> 애플리케이션을 멈추고, 최종적으로 GC에서 제외될 객체를 식별

 

Cleanup -> 살아있는 객체가 적은 Region에 대한 미사용 객체 제거를 수행합니다. 완전히 비워진 Region은 재사용될 수 있게 처리

 

Copy -> GC 대상 Region이었지만, Cleanup 단계에서 완전히 비워지지 않은 Region을 새로운 Region으로 복사하여 Compaction 작업 수행

 

4-6. ZGC (동작 방식 추가 예정)

 

Java 11에서 Linux 환경에서 실험적으로 소개된 확장성이 있는 짧은 지연 시간을 갖는 GC입니다. JDK 14부터는 윈도 MAC에서도 지원합니다. 

 

ZGC는 고가의 모든 작업을 동시에 수행하므로 짧은 지연 시간이 필요한 애플리케이션에 적합합니다.. 또한 색상이 지정된 포인터와 로드 장벽(load barriers with colored pointers)을 사용하여 스레드가 실행 중일 때 동시 작업을 수행하고 힙 사용량을 추적하는 데 사용됩니다.

 

coloring pointer는 ZGC의 중요한 개념입니다. ZGC는 reference의 일부 bit를 사용하여 개체의 상태를 표시합니다. 이는 8MB~16TB 범위의 힙 영역에 대해 가능하며 힙, 루트 세트 크기에 따라 일시 중지 시간이 증가하지 않습니다.

 

 

 

다음은 GC 튜닝하는 방법에 대해서 알아보겠습니다. 감사합니다. 

 

참고 

 

'JAVA' 카테고리의 다른 글

Java Null 처리에 대한 여정  (0) 2023.10.16
Java 8 -> 11 변경점  (0) 2022.12.07
이펙티브 자바  (0) 2022.04.22
JAVA Overloading & Overriding  (0) 2022.02.26
JAVA Enum  (0) 2022.02.24