Study/Java

Garbage Collection의 동작 방식과 종류

jonghne 2024. 2. 8. 23:05

Garbage Collection 이란 ?

Garbage Collection (이하 GC)는 자바의 메모리 관리 기법 중 하나로, Heap 영역에서 제거 대상(Garbage)를 찾아내고 제거해서 힙 메모리를 회수하는 기능을 한다. 

 

일반적으로 C, C++ 와 같은 언어에서는 이와 같은 GC가 없기 때문에 개발자가 직접 메모리 할당과 해제를 직접 관리해야 한다.

 

그러나 Java는 JVM내에 GC가 내장되어 있어 개발자는 메모리 관리에 신경쓰지 않고 개발에만 집중할 수 있다는 장점이 있다.

 

하지만 이런 GC에는 몇가지 단점이 존재하는데, GC가 메모리를 언제 해제하는지 개발자는 정확하게 알 수 없어서 제어하기 힘들다는 점이 있다.

 

또한 GC가 발생하는 동안에는 다른 스레드들이 일을 멈추게 되는 Stop-The-World가 발생한다.

 

이 Stop-The-World는 사용자 입장에서는 오버헤드가 발생한 것이기 때문에 너무 잦은 GC가 발생하면 성능 이슈로 이어질 수 있다. 

 

결론적으로 자바 개발자들은 평소 GC가 메모리를 관리해주기 때문에 메모리 관련해서 신경쓰지 않고 편하게 개발할 수 있지만, 만약 GC로 인해 성능 저하가 생긴다면 GC튜닝을 통해 해결해야 할 수 있기 때문에 GC의 종류와 특징에 대해 잘 알아둬야 한다.

 

Garbage Collection 의 제거 대상

Java의 GC는 도달 능력(reachability)라는 개념을 사용해서 유효한 참조가 있는 객체(reachable object)와 유효한 참조가 없는 객체(unreachable object)를 구분하고, 유효한 참조가 없는 객체에 대해 GC를 수행한다. 

그럼, GC는 유효한 참조가 있는 지 어떻게 알아낼까?

힙의 객체가 참조되는 경우는 다음과 같은 4가지 경우가 있다.

1. 힙 내의 다른 객체에 의한 참조
2. Java 스택의 지역 변수 또는 파라미터에 의한 참조
3. 네이티브 스택 내의 JNI(자바 네이티브 인터페이스)에 의한 참조
4. 메서드 영역의 정적 변수에 의한 참조 

 

힙의 객체가 1번의 경우를 제외한 나머지 경우에 의해 참조 되는 경우를 Root Set이라고 하는데, 이 Root Set으로 부터 시작해서 참조 사슬로 속한 객체들은 유효한 참조가 있는 객체 (reachable object)로 간주한다.

 

반대로 Root Set으로 부터 시작하지 않고 힙 내의 객체들끼리 연결되어 있는 경우를 unreachable object라고 하고 GC의 대상이 된다.

 

Garbage Collection 진행 과정

GC는 다음과 같은 두가지의 가설(전제조건)을 토대로 만들어진다. 

1. 대부분의 객체는 금방 unreachable한 상태가 된다. 
2. 생성된지 오래된 객체에서 생성된지 얼마 안 된 젊은 객체로의 참조는 아주 드물게 발생한다.

 

이러한 가설(전제조건)을 Weak Generational Hypothesis 라고 하는데, 대부분의 GC는 이러한 전제조건의 특징을 살려서 힙 영역을 크게 Young Generation과 Old Generation이라는 물리적인 공간으로 나누고 각 영역 별로 GC를 수행한다.

 

Young Generation

Young Generation 영역에는 새로 생성된 객체가 위치하는 영역으로 대부분의 객체가 금방 unreachable 상태가 되어 제거되고 메모리가 꽉 차면 Minor GC가 발생한다.

 

이 Young Generation 영역은 세부적으로 Eden / Survivor0 / Survivor1 영역으로 나뉘게 된다. 

 

최초 생성되는 객체는 Eden에 머무르다가 메모리가 가득 차면 Minor GC에 의해 제거되고, 살아남은 객체는 Survivor 영역에 번갈아가며 위치하다가 일정 수준의 Age 값에 도달한다면 Old Generation 영역으로 이동하게 된다. 

동작 방식

1. 애플리케이션이 동적으로 생성한 객체가 Eden 영역에 쌓이게 된다.

 

2. 계속 객체가 생성되다가 Eden 영역의 메모리가 꽉 차면 Minor GC가 발생해서 GC 대상인 객체는 제거하고, 살아남은 객체는 Survivor 영역 중 한 곳으로 옮긴다 (현재 활성화 되어 있는)

 

3. Minor GC 이후 다시 Eden 영역에 객체가 가득 차면 또 다시 Minor GC가 발생하고, 이 때 살아남은 객체들은 또다른 Survivor 영역으로 옮긴다. (이 때, 기존 Survivor 영역의 객체도 함께 옮겨진다)

 

4. 이런 방식으로 Survivor 영역을 옮겨가며 생존하는 객체들은 Minor GC가 발생할 때 마다 객체가 살아남은 횟수를 의미하는 Age 값이 증가하게 되고, JVM 별로 설정한 특정 Age값에 도달하면 Old Generation 영역으로 이동하는 Promotion이 발생한다. 

 

Old Generation 

Old Generation은 Young Generation에서 오랫동안 살아남은 객체들이 Promotion을 통해 이동하는 영역이다.

 

이 영역은 대부분 Young Generation 보다 큰 메모리 공간을 할당 받아서 GC가 자주 발생하지는 않는다. 하지만 이 영역에서도 객체가 계속 쌓이다가 메모리가 꽉 차면 GC가 발생하는데 이 것을 Major GC 또는 Full GC라고 한다. 

 

이 Major GC / Full GC 가 발생하면 애플리케이션이 GC를 수행하는 동안 잠시 멈추는 Stop-The-World가 발생한다.

참고 ! 
Minor GC에서도 GC를 하는 순간에는 Stop-The-World 가 발생할 수 있지만, Old Generation 영역 보다 메모리 공간이 작기 때문에 GC를 수행하는 동안 발생하는 Stop-The-World 시간이 비교적 짧다.

 

GC 종류

자바가 발전됨에 따라 Heap 사이즈가 커지게 되었고, 이를 효율적으로 사용하기 위해 다양한 GC 알고리즘이 개발 되었다.

 

Serial GC

싱글 스레드로 동작하는 가장 단순한 방식의 GC이다. CPU 코어가 한개만 있을 때 사용하기 위해 만든 GC이다. 

Serial GC는 Stop-The-World 시간이 다른 GC에 비해 매우 길기 때문에 실무에선 사용하지 않는 것이 좋다. 

Serial GC는 Young Generation에서는 가장 기본적인 GC 알고리즘인 Makr-Sweep을 사용해서 Minor GC를 수행한다. 

 

Mark-Sweep 알고리즘은 객체 그래프 탐색을 통해 객체 참조 정보를 마킹해서 reachable / unreachable 객체를 구분하고, unreachable 객체를 힙에서 제거하는 알고리즘이다. 

 

Old Generation에서는 이 Mark-Sweep 이후 단편화된 메모리를 압축시켜주는 Compact 단계가 추가된 Mark-Sweep-Compact 알고리즘을 사용해서 Full GC를 수행한다.

Mark-Sweep-Compact

  • Mark : 객체 그래프 탐색을 통해 각각의 객체에 대한 참조 정보를 마킹한다. 
  • Sweep : Mark 과정에서 찾은 unreachable 객체를 제거한다. 
  • Compaction : Sweep 과정 이후 파편화 된 메모리 공간을 압축해서 빈 공간을 없앤다.

Serial GC 설정 방법

Java -XX: +UseSerialGC -jar Application.java

 

Parallel GC

Java 8까지 Default로 사용된 GC로, Serial GC와 기본적인 GC 알고리즘은 같지만 Minor GC를 멀티 쓰레드로 수행한다는 차이가 있다 (Full GC는 동일하게 싱글 스레드)

 

주로 멀티 스레드로 대규모 데이터 처리를 하는 애플리케이션을 위해 나온 GC이다. 

Parallel GC 설정 방법

java -XX:+UseParallelGC -jar Application.java

// 사용할 최대 쓰레드 개수를 옵션으로 설정 가능
-XX:ParallelGCThreads=N

 

Parallel Old GC

Parallel GC의 Old Generation 영역에 새로운 알고리즘이 추가된 업그레이드 버전으로, Old Generation의 GC도 멀티 스레드 기반으로 동작한다. 

 

이 Parallel Old GC는 Old Generation 영역의 Full GC를 Mark-Sweep-Compact가 아닌 멀티 스레드 기반의 Mark-Summary-Compaction 알고리즘을 사용한다.  

Mark-Summary-Compaction

1. Mark

- Old Generation 영역을 Region이라는 논리적인 단위로 균일하게 나눈다.

- 여러개의 GC 스레드가 각 Region 영역을 할당 받고, Reachable 객체를 marking 한다

- 이 때, Reachable 객체의 수 / 위치 정보 등의 통계 정보를 계산한다.

 

2. Summary

- 하나의 스레드만 Summary 단계를 수행하고, 나머지 스레드는 애플리케이션을 수행하는 Concurrent 작업을 수행한다.

- Mark 단계에서 수집한 Region 별 통계 정보를 토대로 각 Region의 Reachable 객체의 밀도를 평가한다.

- Reachable 객체의 밀도를 토대로 Dense Prefix를 설정해서 GC의 수행 범위를 결정한다. 

    -> Dense Prefix 기준 왼쪽에 있는 Region은 GC 대상에서 제외되고, 오른쪽 영역의 Region에 대해 GC와 Compaction을 수행한다.

    -> 이 방법은 오래된 객체일 수록 사용될 확률이 높다고 가정하고 GC에서 제외시켜서 Compcation 범위를 줄이는 것이다.  

 

3. Compaction

- 힙을 잠시 멈춰두고 모든 스레드들이 Region을 할당 받아 Compaction 작업을 수행한다. (이 때, Stop-The-World가 발생한다)

- 이 Compaction 작업은 unreachable 객체들을 제거 하고, 살아남은 객체들을 왼쪽으로 모두 몰아서 메모리를 비우는 작업이다.

- 여기서 살아남은 객체가 옮겨지는 Region을 Destination Region, 옮겨질 객체가 포함된 Region을 Source Region이라고 한다 (이는 Summary 단계에서 Dense Prefix 설정 이후 결정된다.)

Parallel Old GC 설정 방법

java -XX:+UseParallelOldGC -jar Application.java
# -XX:ParallelGCThreads=N : 사용할 쓰레드의 갯수

 

CMS GC

CMS (Concurrent Mark Sweep) GC는 Parallel GC와 멀티 스레드를 이용한다는 점은 같지만, Mark Sweep 알고리즘을 Concurrent하게 수행한다는 차이가 있다.

 

대부분의 과정을 애플리케이션 스레드와 동시에 실행되기 때문에 Stop-The-Wolrd 시간이 매우 짧아서, 애플리케이션 응답 속도가 매우 중요한 경우에 이 CMS GC를 사용한다. (Low Latency GC라고도 부른다) 

 

단점으로는 다른 GC 방식보다 메모리와 CPU를 더 많이 사용하고, Compation 단계가 기본적으로 제공되지 않는다. 

시스템 운영되다가 파편화된 메모리가 많아져서 Compaction을 따로 수행해야 한다면 오히려 Stop-The-World 시간이 길어질 수도 있다.

 

CMS GC는 Java 9 버전부터 deprecated 되었고 14버전부터는 사용이 중지된 GC이다.

Java 11 이상을 사용하는 시스템에서는 사용될 일이 없기 때문에 아래의 동작 방식은 참고만 하도록 하자

CMS GC 동작 방식

  • Initial Mark : 클래스 로더에서 가장 가까운 객체들 중 살아있는 객체만 찾는 과정으로 stop-the-world 시간이 매우 짧다
  • Concurrent Mark : Initial Mark 단계에서 찾은 객체에 대해 객체그래프 탐색 하며 참조 정보를 확인한다. 이 단계에서는 애플리케이션 스레드들과 동시에 진행된다. 
  • Remark : Concurrent Mark 단계에서 애플리케이션에 의해 새로 추가되거나 참조가 끊긴 객체가 있는 지 확인한다.
  • Concurrent Sweep : Remark 단계까지를 통해 찾은 unreachable 객체를 제거한다.

G1 GC

G1 (Garbage-First) GC는 서버의 CPU와 메모리 사이즈가 커짐에 따라 개선된 GC로, CMS GC를 대체하기 위해 만들어진 GC이다.

 

CMS GC는 Old Generation 영역에 대해서만 동시에 진행하고 Young Generation 영역은 GC를 수행할 때 Stop-The-World가 발생한다는 문제가 있었다.

 

그에 비해 G1 GC는 객체를 마킹하기 위해 GC를 시작하는 순간 외에는 거의 모든 작업을 애플리케이션과 동시에 처리한다는 장점이 있다.

또한, 기존 GC와는 다르게 힙을 물리적으로 나눠놓지 않고 Region이라는 잘게 나눈 영역으로 관리하기 때문에 작업이 빠르게 진행된다.

 

단점으로는 힙 영역이 작을수록 성능이 안좋아진다는 점이 있다.

G1 GC의 Heap 구조

위의 장점에서 말한것과 같이 G1 GC는 기존 GC들과는 완전히 다른 방식으로 힙 영역을 사용한다

 

기존 GC들은 힙 영역을 물리적으로 고정된 Young Generation과 Old Generation으로 나누어 사용했다.

 

하지만 G1 GC는 Region이라는 고정된 크기의 논리적인 단위를 새로 도입했고, 힙 영역을 여러개의 Region으로 나누어서 역할을 동적으로 부여한다.

 

G1 GC의 힙 영역에는 Eden, Survivor, Old 외에 Homonogous와 Available/Unused 라는 영역이 추가되었다.

 

Homonogous 는 Region 크기의 50%를 초과하는 객체를 저장하는 영역이고, Available/Unused 는 사용되고 있지 않은 영역을 의미한다. 

 

G1 GC의 핵심은 이러한 Region 중에 Garbage의 비중이 높은 Region 순으로 GC를 수행해서 Full GC가 최대한 발생하지 않게 하는 것이다. 

G1 GC 동작 방식

G1 GC도 다른 GC와 마찬가지로 MinorGC와 MajorGC를 나누어서 수행한다. 

 

1. Minor GC (Young GC)

멀티 스레드로 Minor GC를 수행하고, Stop-The-World가 발생한다. 

  • Young Generation Region의 객체가 기준치를 초과하면 Mark-Sweep을 수행하고, 살아남은 객체는 다른 Region으로 옮긴다.
  • 만약 Available/Unused 영역에 옮겨지면 해당 Region은 Young Generation 영역이 되고, 객체가 옮겨지기 전의 영역은 Available/Unused 영역이 된다.

2. Major GC 

  • Initial Mark : Old Region 객체가 참조하고 있는 Survivor Region이 있는지 마킹한다. (Stop-The-World 발생)
  • Root Region Scan : Initial Mark에서 찾은 Survivor Region에서 GC 대상 객체를 찾는다. (멀티 스레드로 동작함)
  • Concurrent Mark : Heap 내의 Old Region에서 생존해 있는 모든 객체들에 대해 Marking 과정을 진행한다. 애플리케이션과 동시에 동작하고 MinorGC와 같이 진행된다.
  • Remark : 애플리케이션을 멈추고 Concurrent Mark 에서 체크한 객체들 중 Unreachable 객체를 식별해낸다. (Stop-The-World 발생)
  • Cleanup & Copy : 애플리케이션을 멈추고 Unreachable 객체가 많은 Region 순으로 순차적으로 Garbage를 제거해 나간다. 이 때 우선 Reachable 객체를 먼저 다른 Region으로 옮겨놓고 이후에 Garbage를 제거해서 여유 공간을 확보한다. 

G1 GC 설정 방법

java -XX:+UseG1GC -jar Application.java

 

참고 

https://s2choco.tistory.com/14

https://coding-factory.tistory.com/829

https://d2.naver.com/helloworld/329631

https://d2.naver.com/helloworld/1329