본문 바로가기

Study

[Java]가비지 컬렉션(GC)

GC(Garbage Collection) 이란?

Java에서 GC 는 JVM 에서 동적 메모리 영역의 사용되지 않는(참조되지 않는) 메모리들을 추적하여 해제하는 것을 말한다. C언어와 같은 로우 레벨 언어에서는 참조가 끝난 메모리를 개발자가 일일이 할당 해제해 줘야 하지만, Java를 비롯해 가비지 컬렉션을 제공하는 언어에서는 그럴 필요가 없다. 메모리가 채워짐에 따라 사용하지 않는 메모리를 GC 가 해제해 주기 때문이다.

Java 에서는 Heap 메모리 내에서 객체가 생성되고 할당되는 영역을 Young Generation, Old Generation 의 두 영역으로 나누었다. 생성된지 얼마 안 된 객체는 Young 영역에 할당되고, 생성되고 시간이 지나도 reachable 상태를 유지하며 살아남은 객체는 Old 영역에 복사된다. 금방 Unreachable 상태로 되는 객체는 Young 영역에 있다가 사라져버리기도 한다. Young 영역에 대한 가비지 컬렉터를 Minor GC, Old 영역에 대한 가비지 컬렉터를 Major GC 라고 부른다.

 

GC 동작방식

GC 는 기본적인 동작 방식으로 아래 두 단계를 따른다.

Stop The World(STW)

 - GC 를 실행하는 스레드를 제외한 모든 스레드들이 작업을 중단( = 애플리케이션 중단)하는 것을 말한다. 가비지컬렉션이 끝나면 애플리케이션이  재개된다. → 성능에 지대한 영향을 미치며, 성능을 위해 GC를 튜닝하는 것은 대부분 STW 시간을 최소화하는 작업을 뜻한다.

Mark and Sweep

- 사용되는 메모리와 사용되지 않는 메모리를 식별하여 제거(메모리 해제)하는 작업을 말한다.

-  사용되고 있는 메모리를 식별하여 구분하는 것을 Mark 라고 하며,  Mark되지 않은 메모리(사용되지 않음으로 식별된 메모리)를 해제하는 작업을 Sweep 이라고 한다. 즉 Mark + Sweep = '사용하지 않는 메모리 식별 후 해체' 를 뜻한다.

+ Compact

- 사용되는(reachable) 메모리, 즉 GC 로부터 살아남은 메모리를 Heap의 시작 주소로 모아 압축하는 과정을 말한다.(GC 종류에 따라 실행하지 않는 GC도 있다)

 

MinorGC 동작방식

가비지컬렉션이 동작하는 원리는 객체가 생성되고 적절한 메모리로 할당되는 모든 과정을 포함한다. 즉 객체의 생명주기를 따라간다. 이는 다음 순서로 이루어진다.

1. 처음 생성된 객체는 Young 영역의 Eden 영역에 할당된다. Young 영역은 그 안에서 다시 1개의 Eden 영역과 2개의 Survivor 영역, 즉 3개의 영역으로 나누어진다. 이 중 Eden 영역은 처음 생성된 객체가 할당되는 곳, Survivor 영역은 Eden 영역에서 GC 를 한번 거친 객체들이 옮겨지는 영역이다. Survivor 영역은 두 개지만 반드시 하나의 영역에만 객체가 존재한다.

2. Eden 영역이 가득 차면, MinorGC 가 실행된다. Eden 영역에서 사용되지 않는(Unreachable) 객체는 메모리 해제되고, 사용되는 객체는 비어있던 Survivor 영역으로 옮겨진다. 같은  시간에 비어있지 않던 Survivor 영역의 메모리 중 사용되지 않는 메모리는 해제되고, 살아남은 메모리는 비어있던 Survivor 영역으로 옮겨지며(Survivor 영역의 생존 객체가 이동하는 시점은 더 조사해 봐야 할 내용이다), 생존 횟수가 많은 객체는 Old Generation 으로 옮겨진다. 하나의 Survivor 영역은 반드시 비어있게 된다.
즉, Eden 영역이 가득 찬 시점을 기준으로 Minor GC가 진행되지만, 그 대상은 사용 중인 Survivor 영역까지 포함한다.
이 과정을 그림으로 나타내면 다음과 같다.

출처 : https://mangkyu.tistory.com/118

 

 

 

Major GC 동작방식

Young 영역에서 age가 임계값만큼 증가한 메모리들은 Old 영역으로 Promotion 된다. Major GC는 객체들이 계속 쌓여 Old 영역의 메모리가 부족해지면 발생한다. Major GC 는 메모리가 가득 차면 GC 를 실행하여 사용되지 않는 메모리를 해제하는 간단한 방식으로 실행되지만, Minor GC가 작은 메모리 공간을 커버하기에 0.5~1초 정도가 걸리는 데 비해 Major GC는 상대적으로 큰 공간을 커버하므로 Minor GC보다 최대 10배 이상의 시간을 사용하기도 한다.

 

GC 의 종류(Java 버전별 GC)

Serial GC 
- Mark and Sweep 방식으로 이루어지는데, Old 영역에서는 Mark and Sweep 에 Compact(reachable 메모리들의 파편을 모아 압축하는 과정) 작업이 더해진다. Serial GC 는 싱글 스레드로 실행되며, CPU 코어가 하나인 시스템에서 돌아가도록 설계되었다. 스레드가 하나 뿐이기 때문에, STW 시간이 가장 길다.

Parallel GC
- Serial GC 와 기본적인 동작 방식은 동일하지만, 멀티스레드를 이용하여 실행되어 기존 GC 의 오버헤드를 크게 줄인 GC이다. Java 8버전까지 기본 GC 로 채택되었다.

CMS GC
- CMS GC 는 Concurrent Mark Sweep GC 의 약자로, Parrallel GC 와 마찬가지로 멀티 스레드를 사용한다. 그러나 Mark 와 Sweep 과정이 어플리케이션과 conccurent 하게 수행되어, STW 로 어플리케이션이 중지되는 시간을 최소화한다.
그렇지만 CMS GC 는 Compact 작업을 하지 않기 때문에 어플리케이션이 장시간 구동되면 메모리 파편이 많아지고, 따라서 장기적인 관점에서 문제를 일으킬 수 있기에 Java 14 버전부터 사용이 중지되었다.

G1 GC(★)
- Java 9 버전 이후부터 Java의 기본 GC로 지금까지 사용되고 있는 GC이다. G1 GC 는 Garbage First GC 의 약자로, Garbage 가 많은 구역을 우선 청소한다고 해서 붙여진 이름이다.
STW 시간을 짧게 유지하기 위해 G1 GC는 메모리 회수를 점진적, 단계적, 병렬적으로 수행한다. G1은 애플리케이션 동작 및 가비지 컬렉션 정보를 추적하여 예측 모델을 구축하고, 가장 효율적인 영역(Garbage 가 많은 영역)을 우선적으로 회수한다. 

G1GC 는 기존의 GC 와 비슷하면서도 다른 특징을 가지고 있는데, 우선 Eden, Survivor, Old 영역 외에도 Humonogous 영역과 empty(비어있는)영역을 추가로 가지고 있다. 또 기존 GC 에서 물리적으로 메모리 공간을 나누어 Young 영역과 Old 영역을 구분했다면, G1 GC 에서는 물리적인 메모리의 분할 없이 균등하게 분할된 영역(=Region) 이라는 개념을 도입하고 각 Region에 논리적인 역할을 동적으로 부여하여 구분한다. 또한 G1 은 가비지가 가장 많은 Region 에 대해 메모리 회수를 병렬적으로 수행하여 일시 정지 시간을 최소화한다.

G1 GC 힙 레이아웃

 

일반적으로 새로 할당되는 객체는Eden 영역 (빨간 영역)에 할당되지만, 크기가 큰 객체의 경우 Old 영역에 할당되며 이 경우 Humonogous 영역에 할당될 수 있다.(H)
그렇다면 G1 GC 에서 어떤 방식으로 메모리를 수거해 가는지 알아보자.

 

G1 GC 의 가비지 컬렉션 주기

 

G1 GC 의 메모리 확보는 가장 효율적인(Garbage가 많은) Young 영역에서 집중적으로 일어나고, Old 영역의 메모리 확보는 가끔 일어난다. 위 다이어그램은 Young 영역의 메모리를 회수하는 Young-Only Phase 와, Young 영역과 Old 영역의 메모리를 동시에 확보하기 위한 혼합 컬렉션 단계인 Space Reclamation(공간 재확보) Phase 가 번갈아가며 나타나는 G1 GC 의 주기를 나타낸다. 이는 한번의 회수 단계에 두 페이즈(Young-Only Phase , Space Reclamation Phase) 를 포함하는 것을 나타내는 것이 아니다.

위 컬렉션 주기를 순서대로 뜯어보자.

 

1. 그림과 같이 Young-Only Phase에서 Eden 영역이 가득 찰 때마다 G1 은 Normal Young Collection(Young 영역의 일반 가비지 컬렉션)을 실행한다. 이 단계에서는 가득 찬 Eden 영역의 메모리 중 일부를 해제하고 일부를 Survivor 영역으로 이동시키며 age 임계값을 넘긴 객체는 Old 영역으로 이동시키는 과정이 포함된다.

Eden 영역이 가득 찼을 때 발생하는 Normal Young Collection

 

2. 이 과정을 반복하며 Old 영역의 점유율이 임계값을 상회했을 때(Old gen occupancy exceeds threshold), Space Reclamation Phase 의 전환이 시작된다.
→  Concurrent Start : Normal Young Collection 을 수행하는 것 외에도, Old 영역에서 Reachable 객체를 마킹하는 작업( Concurrent Marking)을 시작한다. Space Reclamation Phase 의 준비를 위한 단계이다. 동시 마킹 작업은 Remark 와 CleanUp 의 두 STW 단계로 마무리된다.

Old 영역의 임계값 초과

 

3. Remark : STW 가 발생하며, 마킹 작업을 마무리하고 비어있는 영역을 회수하며 내부 데이터 구조를 정리하는 단계이다. Remark 단계 후에 G1은 Old 영역에서 동시에 회수 가능한 공간을 계산하며, 이 작업은 CleanUp 단계에서 마무리된다.

 

 

4. CleanUp : 두번째로 STW 가 발생하며, 이 단계에서는 Space Reclamation Phase 가 실제로 뒤따를지 여부를 결정한다. . Space Reclamation Phase 가 시작될 경우 Young-Only Phase 는 종료된다.

 

5. Space Reclamation Phase 는 Young Generation Collection 과 Old Generation Collection 의 혼합 컬렉션으로 구성된 단계이며, Old 영역 뿐만 아니라 Young 영역의 메모리도 회수해 간다. 이 단계는 G1이 더이상의 Old 영역의 메모리를 확보하는 것이 가치가 없다고 판단할 때 종료된다.

 

6. Space Reclamation Phase 후 컬렉션 주기는 Young-Only Phase 로 다시 시작된다. 이를 반복하는 과정에서 어플리케이션의 메모리가 부족해지면 GC 는 다른 컬렉터와 마찬가지로 Full GC 를 수행한다.
(Full GC 와 그로 인해 발생하는 STW 에 대한 정보는 레퍼런스를 찾기가 어려워 다음에 다시 정리해 보기로 한다.)

 

 

ref:
https://mangkyu.tistory.com/118
https://mangkyu.tistory.com/119
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC
https://docs.oracle.com/en/java/javase/12/gctuning/garbage-first-garbage-collector.html#GUID-DA6296DD-9AAB-4955-8B5B-683651936155