Search
Duplicate

Garbage Collection

Garbage Collection이란?

메모리 관리 기법 중의 하나로 프로그램이 동적으로 할당했던 메모리 영역 중 필요없게 된 영역을 해제하는 기능이다.
⇒ 필요없게 된 영역: 어떤 변수도 가리키지 않게 된 영역

장점

프로그래머가 동적으로 할당한 메모리 영역의 전체를 완벽하게 관리하지 않아도 GC에 의해 관리가 된다.
다음과 같은 버그를 줄이거나 없앨 수 있다.
1.
유효하지 않은 포인터 접근: 이미 해제된 메모리에 접근하는 버그
2.
이중 해제: 이미 해제된 메모리를 또 다시 해제하는 버그
3.
메모리 누수: 더 이상 필요하지 않은 메모리가 해제되지 않고 남아있는 버그

단점

어떤 메모리를 해제할지 결정하는데 비용이 든다.
객체가 필요없어지는 시점을 개발자가 알고 있어도 GC알고리즘이 메모리 해제 시점을 추적해야 하기에 해당 작업은 오버헤드가 된다.
쓰레기 수집이 일어나는 타이밍이나 점유 시간을 미리 예측하기 어렵다. 그렇기에 프로그램이 예측 불가능하게 일시적으로 정지할 수 있으며 이러한 문제는 실시간 시스템에 적합하지 않다.
할당된 메모리가 해제되는 시점을 알 수 없다.

주의점

GC가 실행할때는 stop-the-world(이하stw)(2)가 발생한다.
이 stw가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈추는데, GC 작업을 모두 완료한 뒤 중단했던 작업들이 재개된다. stw는 어떠한 GC알고리즘을 사용하더라도 발생하며, GC 튜닝은 이 stw의 소요시간을 줄이는 것에 있다.
Java에서는 메모리를 명시적으로 해제하지 않는데, 명시적으로 헤제하기 위해선 다음과같은 방법이 있다.
해당 객체 참조에 null지정
System.gc() 호출
여기서 null 지정은 문제가 되지 않는다. 하지만, System.gc()는 시스템 성능에 매우 큰 영향을 끼치기에 절대 사용해서는 안된다.
자바는 개발자가 직접 메모리를 명시적으로 해제하지 않으며, GC가 더 이상 필요없는 객체를 찾아서 지우는 작업을 하는데, 이 GC는 다음과 같은 전제하에 만들어졌으며 그러한 전제를 weak generational hypothesis라 한다.
대부분의 객체는 금새 접근 불가 상태(unreachable)가 된다.
오래된 객체에서 젊은 객체로의 참조는 매우 적게 존재한다.
이 전제 조건의 장점을 최대한 살리기 위해 HotSpot VM(1)에서는 크게 두 개로 물리적 공간을 나눴는데 이게 Young 영역Old 영역이다.
Young 영역
⇒ 새롭게 생성한 객체의 대부분이 위치하는 영역. 대부분의 객체는 생성 후 금새 접근 불가 상태가 되기에 많은 객체가 해당 영역에 생성되었다 사라지는데, 이 때 Minor GC가 발생한다고 말한다.
Old 영역
⇒ 접근 불가 상태가 되지 않고 Young 영역에서 살아남은 객체는 Old 영역으로 복사된다. 대부분 Young 영역보다 크게 할당하며 크기가 큰 만큼 GC는 적게 발생한다.
이 영역에서 객체가 사라질때는 Major GC(or Full GC)가 발생한다고 말한다.

Old영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행하는데, GC 방식에 따라 처리 절차가 달라진다. GC 방식은 JDK 7을 기준으로 5가지 방식이 있다.

Garbage Collection 종류

Serial GC(-XX:+UseSerialGC)

: Old 영역의 GC는 mark-sweep-compact라는 알고리즘을 사용한다.
1.
Old 영역에 살아있는 객체를 식별(Mark)한다.
2.
힙(heap)의 앞 부분부터 확인하여 살아 있는것만 남긴다(Sweep)
3.
각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 없는 부분으로 나눈다.(Compaction)
이러한 방식의 GC는 적은 메모리와 CPU코어 개수가 적을 때 적절한 방식이다.

Parallel GC(-XX:+UseParallelGC)

Serial GC와 기본적은 알고리즘은 같다.
하지만, Parallel GC는 GC를 처리하는 쓰레드가 여러개이다. 그렇기에 Serial GC보다 속도면에서 빠르다. Parallel GC는 메모리가 충분하고 코어의 갯수가 많을 때 유리한데, Throughtput GC라고도 부른다.

Parallel Old GC(-XX:+UseParallelOldGC)

JDK 5 update 6 부터 제공한 GC 방식.
Parallel GC와 비교해 Old 영역의 GC알고리즘만 다르다. Mark-Summary-Compaction 단계를 거치는데, 위에서 소개한 Mark-Sweep-Compaction 알고리즘의 Sweep 부분이 달라졌으며 약간 더 복잡해진다.

CMS GC(-XX:+UseConcMarkSeppGC)

모든 애플리케이션의 응답 속도가 매우 중요할 때 사용하는 GC로 Low Latency GC라고도 부른다.
GC과정 중 진행중이던 스레드가 정지하지 않기 때문에 stop-the-world 시간이 짧다.
1.
Initial-mask 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아있는 객체만 찾는다.
2.
Concurrent Mark 단계에서 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가며 확인한다. 이 때 다른 스레드가 실행 중인 상태에서 동시에 진행된다.
3.
Remark 단계에서 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
4.
Concurrent Sweep 단계에서 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행한다.
하지만 이 CMS GC도 다음과 같은 단점이 있다.
다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
Compaction 단계가 기본적으로 제공되지 않는다.
조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC방식의 stw시간보다 더 오래걸린다.
그렇기에 CMS GC를 단순히 빠르다고 쓰면 안되며, Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.

G1 GC(Garbage First)

지금까지 설명한 Young / Old 영역을 사용하는 방식과는 완전히 다르다.
G1 GC는 위 그림처럼 바둑판의 각 영역에 객체를 할당한 뒤 GC를 실행한다. 그렇게 실행이 되다가
해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.
이런 방식은 Young 의 세가지 영역에서 Old 영역으로 데이터가 이동하는 단계가 사라진 GC방식이라고 보면되며 CMS GC를 대체하기 위해 만들어 졌다.
성능적으로는 가장 뛰어나며 JDK6에서는 얼리억세스로만 사용이 가능했으나 JDK 7 부터 정식으로 포함되어 사용이 가능해졌다.

ZGC (The Z Garbage Collector)

The Z Garbage Collector, also known as ZGC, is a scalable low-latency garbage collector → 확장 가능하며 낮은 레이턴시의 가비지 컬렉터(GC)
기존 JVM의 여러 Garbage Collector들은 모두 Stop-The-World로 인해 성능에 영향을 주고 있었다.
그래서 이러한 성능을 개선하고자 여러 종류의 GC들이 나왔고, ZGC도 이러한 개선의 일환이며 JDK11에서 선보이기 시작했다. 이러한 ZGC는 64bit 주소 체계에서 사용되지 않는 18bit를 사용해서 gc의 성능을 높힌다. (아래 Colored Pointers 그림 참조)
Finalizable: finalizer를 통해서만 참조되는 Objectd의 Garbage
Remapped: 재배치 여부를 판단하는 마크
Marked 1(or 0): Live Object
1.
특징
Low-Latency
⇒GC 정지 시간이 10ms를 초과하지 않는다.
Scalable
⇒ Heap의 크기가 증가하더라도 정지 시간이 증가하지 않는다.
8MB ~ 16TB 크기까지 다양한 범위의 heap 처리가 가능하다.
모든 종류의 고비용 작업을 동시(concurrently)작업하고 이때 애플리케이션 스레드 실행을 중지하지 않는다.
동시에 기동할 스레드의 수를 너무 크게 설정할 경우 (튜닝) GC가 애플리케이션의 CPU시간을 다 뺏기에 처리량이 오히려 떨어지고, 너무 낮게 설정할 경우 쓰레기가 쌓이는 속도가 더 빠르다.
2.
ZGC의 목표
G1 보다 낮은 Latency를 가지고 G1에 뒤쳐지지 않는 처리량을 가지는 GC를 만들자.
GC 정지 시간이 10ms를 초과해서는 안된다.
다양한 크기의 heap을 다룰 수 있어야 한다.
G1보다 애플리케이션 처리량이 15%이상 떨어져서는 안된다.
3.
주요 원리
colored pointers, load barriers를 함께 사용하여 GC를 위한 기능/최적화 기반을 마련한다.
이를 통해 Thread가 동작하는 중간에도 ZGC가 객체 재배치같은 작업 수행이 가능하다.
ZGC에서는 메모리를 ZPages라 불리는 영역으로 나누고 동적 사이즈로 2MB의 배수가 동적으로 생성 및 삭제될 수 있다.
→ ZGC Heap은 위 그림과 같이 다양한 사이즈의 영역이 여러개 발생할 수 있다.
→ ZGC가 compaction된 후, ZPage는 ZPageCache라 불리는 캐시에 삽입된다.
→ 캐시 안의 ZPage는 새로운 Heap 할당을 위해 재사용할 준비를 한다.
→ 메모리를 커밋과 커밋하지 않는 작업은 비용이 크기에 캐시의 성능에 중요한 영향을 끼친다.
4.
주의점
ZGC를 사용하기 위해서는 Compressed OOP를 사용해서는 안된다. 0으로 채워지는 padding 영역을 zgc에서 사용하기 때문이다. (bit Object Pointer의 Unsed bits)
heap 사이즈가 32Gbytes 이상으로 크게 써야할 경우 zgc를 사용하는게 좋다.
ZPage는 사용되지 않는 메모리 집합을 정책에 따라 커밋 해제하여 OS로 반환하는데, 일반적으로 LRU(Least Recently Used) 방식을 사용하고, page 크기로 구분하기에 메모리를 해제하는 방법은 비교적 간단하지만, ZPage를 제거할 시기를 결정하는데 주의해야 한다.
1.
일정 시간이 지나면 제거되도록 설정 : -XX:ZUncommitDelay=<seconds>(default 300sec) 으로 간단하게 정책 제공이 가능하다.
2.
새 옵션을 추가하지 않고 GC가 일어나는 빈도에 기초해 메모리 해제 주기를 설정할 수 있다.
5.
사용법
ZGC 활성화
XX:+UnlockExperimentalVMOptions XX:+UseZGC 를 사용해 활성화할 수 있다.
힙(heap) 크기 설정
-Xms<Size>가장 중요한 설정 옵션으로 설정한 힙 크기 아래로 줄어들지 않게 지정된다.
⇒ heap이 애플리케이션의 라이브셋을 수용할 수 있어야 한다.
⇒ heap에서 GC가 돌아가는 동안 할당을 처리할 수 있을만큼의 여유공간이 있어야 한다.
⇒ -Xms와 -Xmx가 동일한 경우, 이 기능이 암시적으로 비활성화 된다. 만약 명시적으로 비활성화 하기위해서는 -XX:-ZUncommit을 사용하면 된다.
동시에 가동하는 GC 스레드의 수 설정
XX:ConcGCThreads=<number> 로 설정할 수 있다.
⇒ ZGC는 휴리스틱(3)을 통해 이 값을 자동으로 선택한다. 너무 많거나 작을 경우 문제가 발생하기에 적절한 스레드 갯수를 설정하는게 중요하다.
GC 로깅
Xlog:<tag set>,[<tag set>, ...]:<log file>
⇒ 기본 로깅 활성화는 -Xlog:gc:gc.log 으로 가능하다.
⇒ 튜닝및 성능 분석에 사용할 로깅은 -Xlog:gc*:gc.log 으로 가능하다.
6.
JDK 버전별 ZGC 변화
JDK 11
ZGC 초기버전 등장
클래스 언로딩은 지원되지 않는다(-XX:+ClassUnloading 효과없음)
JDK 12
동시 클래스 언로딩 지원
추가적인 GC 정지시간 단축
JDK 13
최대 heap 사이즈가 4TB에서 16TB로 증가
사용되지않는 메모리 uncommitting 지원
-XX:SoftMaxHeapSize 명령어 지원
Linux/AArch64 플랫폼 지원
Time-To-Safepoint 감소
JDK 14
맥 OS 지원
윈도우 OS 지원
작은 heap(8M 이하)지원
JFR leak profiler 지원
제한적이고 불연속적인 주소 공간 지원
-XX:+AlwaysPreTouch 를 명령어로 병럴 pre-touch 지원
안정성 및 성능 향상
JDK 15
프로덕션 준비 완료
NUMA 인식 향상
동시할당 향상
CDS(Class Data Sharing) 지원
NVRAM의 heap 위치 지원
압축된 클래스 포인터 지원
증분 uncommit 지원
JFR 이벤트 추가
JDK 16
병렬 스레드 스택 스캔(JEP 376)
: ZGC의 thread-stack processing이 safepoint에서 concurrent phase로 이동
내부 재배치 지원
성능 향상(포워딩 테이블 할당 및 초기화)

참고 자료

해당 포스팅은 다음 자료를 참고하여 작성되었다.
NAVER D2 - Garbage Collection (https://d2.naver.com/helloworld/1329)
(1): 미국의 Longview Technologies LLC라는 회사에서 1999년에 처음 발표한 JVM으로 Hot한 Spot을 찾아 해당 부분에서는 JIT 컴파일러를 사용하는 방법으로 내부적으로 프로파일링을 통해 핫스팟을 찾아내고, 해당 부분에 대한 네이티브 코드를 생성한다.(자세히)
(2): GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것
(3): 휴리스틱(heuristics) 또는 발견법이란 정보나 시간이 충분하지 않은 경우 체계적이거나 합리적인 판단이 굳이 필요 없는 상황에서 빠르게 사용할 수 있게 구성된 간편 추론의 방법(자세히)