본문 바로가기

IT/JAVA

GC (Garbage Collection)의 동작 방식에 대해서

GC(Garbage Collection)는 Java에서 메모리를 자동으로 관리하는 기능입니다.
따라서 자바는 메모리를 명시적으로 해제하지 않습니다. GC가 알아서 해주기 때문입니다.

 

GC는 JVM의 Heap 영역에 존재하고 있습니다. 그리고 Heap 영역은 객체들이 저장되는 곳이므로 GC는 객체를 관리하는 것이라고 할 수 있습니다.

 

Heap 구조

그렇다면 먼저 GC가 관리하는 Heap의 구조에 대해 살펴보겠습니다.

JVM의 Heap 영역은 처음 설계될 때 다음을 전제로 설계되었습니다.

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

이 전제들은 JVM GC 설계자들이 대부분의 객체가 생겨나자마자 쓰레기가 된다는 것을 경험적으로 알게된 지식을 바탕으로 나오게 되었습니다

ps. 이러한 것을 'Weak Generational Hypothesis'라고 합니다.

 

Heap 영역은 이 전제 조건의 장점을 최대한 살리기 위해서 GC 관리 영역을 크게 Young 영역과 Old 영역으로 나누었습니다.

 

Heap의 구조

먼저, Young 영역은 새롭게 생성한 객체이 위치하게 됩니다.

대부분의 객체가 금방 접근 불가 상태가 되기 때문에 많은 객체가 Young 영역에 생성되었다가 사라지는데, 이를 Minor GC가 발생한다고 말합니다.

 

그 다음, Old 영역은 Young영역에서 살아남은 객체가 존재하는 곳입니다.

Old 영역은 꽉찬 경우에 GC가 발생하는데, 이를 Major GC라고 말합니다.

대부분 Young 영역보다 크게 할당되기 때문에, 크기가 큰 만큼 Young 영역보다 GC가 적게 발생합니다.

 

마지막으로 Permanent는 프로그램이 끝날 때까지 메모리를 차지하고 있는 공간입니다. 따라서 GC의 관리를 받지 않습니다.

 

GC 동작 방식

다음으로 GC의 동작 방식에 대해 알아보겠습니다.

 

그 전에, 객체가 메모리에서 제거되는 기준은 무엇일까요?

만약 현재 사용중인 객체를 메모리에서 제거해버린다면, 프로그램이 정상적으로 실행되지 않을 것입니다.

때문에 먼저 메모리에 있는 객체가 현재 사용중인지 아닌지를 구분한 후, 다른곳에서 참조되지 않는 객체를 메모리에서 제거하게 됩니다.

 

기본적으로 GC가 실행된다고 하면 2가지 공통적인 단계를 따르게 됩니다.

1. Stop-The-World
2. Mark and Sweep

Stop-The-World란, GC가 실행되는 동안 JVM이 애플리케이션 실행을 멈추는 것을 의미합니다. 

Stop-The-World가 발생하면, GC를 실행하는 스레드를 제외한 나머지 스레드는 작업을 멈추게 됩니다. 

 

따라서 GC가 발생하면 애플리케이션이 아무 동작을 할 수가 없게 되는데,

어떤 GC를 사용하든 Stop-The-World가 발생하기 때문에, Stop-The-World을 최소화하는 것이 중요합니다.

 

다음으로 Mark and Sweep이란,  GC가 실행될 때 사용하고 있는 객체를 식별하는 작업(Mark)과 사용되지 않는 객체(Mark되지 않은 객체)를 제거하는 작업(Sweep)을 의미합니다.


Minor GC 동작 방식 

Minor GC가 일어나는 Young 영역Eden영역Survivor영역으로 구성되고, Survivor영역은  Survivor1과 2로 구성됩니다.

 

새로 생성된 객체는 Eden 영역에 존재하게 되는데, 이 Eden 영역이 꽉찬 경우에 Minor GC가 실행됩니다.

 

Eden 영역에서 GC가 발생한 후에 살아남은 객체는 Survivor 영역 중 하나로 이동하게 됩니다. 

다만 여기 부분이 헷갈리는데,

 

1. Minor GC가 Eden 영역과 Survivor 영역 모두에서 발생하고, 각각 살아남은 객체는 비어있던 Survivor 영역(예를 들어, 1에 객체가 존재했다면 2에)으로 이동합니다. 이동하지 않은 객체들은 제거됩니다.

 

2. Minor GC가 Eden 영역에서 발생하고, Eden 영역에서 살아남은 객체는 비어있는 Survivor 영역이 아닌 이미 객체들이 존재하는 Survivor 영역으로 이동하게 됩니다. 이 때, Survivor 영역이 가득 차게 되면, Survivor 영역에도 Minor GC가 발생하게 되어 이때 살아남은 객체는 비어있는 Survivor 영역으로 이동하게 되고, 이동하지 않은 객체들은 제거됩니다.

 

둘 중에 뭐가 정확한건지 잘 모르겠지만,

결국 Minor GC 발생시, 살아남은 객체들을 제외하고는 제거된다는 것은 같습니다.

 

그리고 이 과정을 반복하면서 오래 살아남아 있는 객체Old영역으로 이동하게 됩니다.

 

그럼, 오래 살아남았다는 것은 무엇일까요?

각 객체에는 Minor GC에서 살아남은 횟수를 기록하는 age bit를 가지고 있습니다.

그리고 이 age bit는 Minor GC가 발생할 때마다 1씩 증가하게 되는데, age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되면 Old 영역으로 객체가 이동하게 됩니다.

ps. 처음 Eden에 생성될 때는 0이겠죠?


Major GC 동작 방식 >

이어서 Major GC에 대해 알아보겠습니다.

Major GC가 발생하는 Old 영역데이터가 가득차면 GC를 실행합니다.

 

Major GC는 다양한 방식이 존재합니다. (살펴보니까 Minor GC의 알고리즘도 다뤄집니다 :))

 

1. Serial GC

Serial GC는 앞서 말한 Mark-Sweep에 이어 Compaction를 진행하는 알고리즘을 사용합니다.

Compaction이란, Sweep 후 메모리 단편화를 방지하기 위해 파편화된 메모리 영역을 앞에서부터 채워나가는 작업입니다. 

ps. 메모리 단편화란 사용 가능한 메모리 공간은 충분하지만 할당이 불가능한 상태를 의미합니다.

 

Serial GC는 순차적으로 동작합니다. GC를 처리하는 스레드가 하나이기 때문입니다.

 

2. Parallel GC

Parallel GC는 Serial GC와 알고리즘이 동일하지만, Minor GC에서 여러 개의 스레드를 이용하는 방식입니다.

하나가 하던 일을 여러 명이 나누어 맡아 하기 때문에 한 개의 스레드만 사용했을 때보다 GC 프로세스가 더 빠르게 동작하게 됩니다.  

결과적으로 Stop-The-World 시간이 줄어듭니다.

 

3. Parallel Old GC

Parallel Old GC는 Parallel GC와 Old 영역이 처리되는 방식이 다릅니다.

우선 Old GC도 여러 개의 스레드를 이용하게 되었습니다. 

그리고 Mark-Sweep-Compaction 알고리즘이 아닌 Mark-Summary-Compaction 알고리즘을 사용합니다.

Summary는 앞서 GC가 수행된 영역에서 살아있는 객체를 식별한다는 점에서 sweep 단계와 다릅니다.

 

4. CMS(Concurrent Mark Sweep) GC

CMS GC는 이름에서도 알 수 있듯이 Mark - Sweep 과정을 Concurrent하는 방식입니다.

즉, 애플리케이션 스레드와 GC 스레드가 동시에 실행함으로써 Stop-The-World 시간을 최소화합니다.

 

Minor GC는 Paraller과 비슷하지만, Major GC는 4단계를 거치게 됩니다.

  1. Initial Mark: 얕은 탐색으로 참조되는 객체를 마킹합니다. 이 때는 Stop-The-World가 발생하지만, 탐색 깊이가 얕기 때문에 시간이 짧습니다.
  2. Concurrent Mark: 애플리케이션 스레드를 중지하지 않으므로 Stop-The-World가 발생하지 않고, 이전 단계에서 마킹된 객체들이 참조하는 다른 객체들을 따라가면서 추가적으로 마킹합니다.
  3. Remark: 이전 과정에서 변경된 사항이 없는지 다시한번 마킹하며 확정짓게 됩니다. Stop-The-World가 발생하기 때문에 지속 시간을 줄이기 위해 멀티 스레드로 수행합니다.
  4. Concurrent Sweep: Stop-The-World을 발생하지 않고 마킹되지 않은 객체를 제거합니다.

Stop-The-World가 최대한 덜 발생하도록 하지만,  Compaction 과정이 기본적으로 제공되지 않기 때문에 메모리 단편화 문제가 발생할 수 있습니다. 

 

5. G1(Garbage First) GC

마지막으로 G1GC는 CMS를 대체하기 위해 만들어졌습니다. Heap을 Young, Old 영역을 물리적으로 명확하게 구분짓지 않고, Region 이라는 논리적인 영역으로 나누어 관리합니다.

Region은 상태에 따라 역할(Eden, Survivor, Old)이 동적으로 변하게 됩니다.

 

기본적으로 G1GC는 Young-Only 단계와 Space Reclamation 단계를 반복하면서 수행하는 구조로 진행됩니다.

 

- Young Only: Minor GC만 수행하다 한정된 Old 영역의 비율이 넘으면 Major GC가 수행됩니다. 

Minor GC는 이전과 같이 Young 영역에서 살아있는 객체를 Survivor 영역이나 Old 영역으로 이동시키는 과정을 수행하며, 멀티 스레드로 동작합니다.

 

Major GC는 5단계로 구분되어 있습니다.

  1. Initial mark
  2. root region scan
  3. concurrent mark
  4. remark
  5. copy/cleanup

 

제대로 이해하지 못했기 때문에 단계별로 설명하긴 어렵지만, 간단하게 이해한걸 말해보자면

마킹을 수행하면서 사용하지 않는 객체가 많은 순으로 Region을 정리하여 공간을 만들어내는 방식이라 할 수 있습니다.

특히 살아있는 객체가 없는 Region은 remark단계에서 먼저 제거해버림으로써  메모리 여유 공간을 많이 확보합니다. 그 결과로 GC의 빈도가 줄어들게 됩니다.

 

그리고 CMS GC와 같이 Initial mark, remark 단계에서 Stop-The-World가 발생합니다.

 

- Space Reclamation(공간회수): Young Only 단계가 끝나고 시작되는 단계로, Mixed GC가 수행됩니다.

Mixed GC는 Young 영역과 Old 영역의 사용하지 않는 객체를 수거합니다.  

이 단계가 끝나면 다시 Young Olny 단계로 돌아가 Minor GC를 수행합니다.

 

G1GC는 전체 Heap에 대해서 탐색하지 않고 부분적으로 Region 단위로 탐색하여 각각의 Region에서만 GC가 발생하도록 합니다.

또한 CMS와 다르게 Compaction을 통해 메모리 단편화를 방지합니다. (Coompaction도 Region별로 이루어집니다.)

 


더 다양한 방법이 존재하겠지만, 대표적으로 언급이 많이되는 방법에 대해서 다뤄보았습니다.

 

기본적으로 GC는 null인 상태의 메모리를 먼저 제거하기 때문에

의도적으로 객체를 null로 만들어 GC가 제거하게 만들거나, 아예 직접 GC를 선언하는 방법도 있습니다.

하지만 GC는 한번 실행될 때마다 CPU 사용량이 크기 때문에 신중하게 생각해야 합니다.

 

점점 하드웨어가 발전함에 따라 Heap의 크기가 커지고, 그로인해 GC 발생시 더 오래 걸리게 되기 때문에 Stop-The-World를 줄이고자 개선된 방식이 나오고 있는데,

그동안 GC하면 알아서 처리! 라는 생각을 가지고 있어서 GC에 대해 자세히 알아볼 생각을 하지 못했습니다. 
그치만 이번에 GC에 대해 알아보면서 환경에 맞게 어떤 GC를 선택할 것인지 고민을 해야할 필요가 있다는 것을 느끼게 되었습니다.

 

보라색 부분은 이해를 잘 하지 못한 부분입니다 ;( 피드백 부탁드립니다. 감사합니다 :)