
- 가비지 컬렉션(Garbage Collection) 1편에 이어 GC에 대해 알아보자.
Generational GC
- GC는 두 가지 가정 하에 만들어졌다.
1.대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
2.오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
-
이러한 가설을 Weak generational hypothesis라 한다.
-
실제 통계로도 생성된 객체의 98%의 객체가 곧바로 쓰레기 객체가 된다고 한다.
-
이 가설의 장점을 최대한 살리기 위해서 HotSpot VM에서는 크게 2개로 물리적 공간(Young / Old)을 나누었다.
-
이러한 경험적 사실들을 바탕으로 Generational GC가 디자인 되었다.
-
우선 2번째 가설에 대해 살펴보면
Old 영역에는 512바이트의 덩어리(chunk)로 되어 있는 카드 테이블(card table)이 존재한다. -
카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다.
-
Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고
이 카드 테이블만 뒤져서 GC 대상인지 식별한다.

-
힙을 Young Generation 영역과 Old Generation 영역으로 나눈 뒤
Young Generation 영역을 주기적으로 청소하고
상대적으로 오랜 기간 사용되는 객체는 Old Generation으로 보내버린다.
그리고 Old 영역에 공간이 부족하게 될 때만 Old영역을 청소를 하게 된다. -
이렇게 함으로써 GC는 매번 힙 전체를 청소할 필요가 없어진다.
-
이러한 디자인에는 성능 상 이점이 2가지가 있다.
-
첫째로 Young Generation은 Old Generation 보다 사이즈가 작고 힙 공간의 일부분이기 때문에
GC가 전체 영역을 처리하는 것보다 시간이 덜 걸린다. -
즉 stop-the-world로 애플리케이션이 중지되는 시간이 짧아진다.
-
비록 자주 GC가 작동하지만 stop-the-world로 길게 한 번 멈추는 것보다
짧게 여러 번 멈추는 것이 더 이익이라고 한다. -
둘째로 Young Generation 영역을 한 번에 모두 비우기 때문에
이 Young Generation 부분에 연속된 여유 공간이 만들어 진다.
이 작업을 Compacting이라고 한다. -
만약 GC가 군데 군데 골라서 객체를 제거했다면
메모리 파편화(memory fragmentation)이 발생하여 연속된 큰 데이터가 들어갈 공간이 부족해지게 된다.
Young 영역에 대한 GC
-
Minor GC 동작 방식은 가비지 컬렉션(Gabage Collection) 1편 - Minor GC를 참고하자.
-
여기서는 Eden 영역에 대해 더 알아보자.
-
참고로 HotSpot VM에서는 보다 빠른 메모리 할당을 위해서 두 가지 기술을 사용한다.
-
하나는 bump-the-pointer라는 기술이며
다른 하나는 TLABs(Thread-Local Allocation Buffers)라는 기술이다. -
bump-the-pointer는 Eden 영역에 할당된 마지막 객체를 추적한다.
-
마지막 객체는 Eden 영역의 맨 위(top)에 있다.
-
그리고 그 다음에 생성되는 객체가 있으면 해당 객체의 크기가 Eden 영역에 넣기 적당한지만 확인한다.
-
만약 해당 객체의 크기가 적당하다고 판정되면 Eden 영역에 넣게 되고 새로 생성된 객체가 맨 위에 있게 된다.
-
따라서 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 매우 빠르게 메모리 할당이 이루어진다.
-
그러나 멀티 쓰레드 환경을 고려하면 이야기가 달라진다.
-
Thread-Safe하기 위해서 만약 여러 쓰레드에서 사용하는 객체를 Eden 영역에 저장하려면
락(lock)이 발생할 수 밖에 없고 lock-contention 때문에 성능은 매우 떨어지게 될 것이다. -
HotSpot VM에서 이를 해결한 것이 TLABs이다.
-
각각의 쓰레드가 각각의 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것이다.
-
각 쓰레드에는 자기가 갖고 있는 TLAB에만 접근할 수 있기 때문에
bump-the-pointer라는 기술을 사용하더라도 아무런 락이 없이 메모리 할당이 가능하다.
Old 영역에 대한 GC
-
Young 영역에서의 GC는 위애서 설명한 방식을 사용한다.
-
Old 영역에서의 GC는 방식에 따라 처리 절차가 달라진다.
-
Old 영역에서는 기본적으로 데이터가 가득 차면 GC를 실행한다.
-
GC 방식은 JDK 7을 기준으로 알아보자.
-
Serial GC
-
Parallel GC
-
Concurrent Mark & Sweep GC(이하 CMS)
-
G1(Garbage First) GC
-
이 중에서 운영 서버에서 절대 사용하면 안 되는 방식이 Serial GC다.
-
Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식이다.
-
Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어진다.
Serial GC (-XX:+UseSerialGC)
-
Old 영역의 GC는 mark-sweep-compact이라는 알고리즘을 사용한다.
-
이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다.
-
그 다음에는 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남긴다(Sweep).
-
마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서
객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction). -
Serial GC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이다.
Parallel GC (-XX:+UseParallelGC)
-
Parallel GC는 Serial GC 와 기본적인 알고리즘은 같다.
-
Minor GC, Full GC 모두 All Stop인건 Serial GC 와 같다.
-
그러나 Serial GC는 GC를 처리하는 쓰레드가 하나인 것에 비해
Parallel GC는 GC를 처리하는 쓰레드가 여러 개이다. -
즉 Minor GC와 Full GC 모두 멀티쓰레드를 사용한다.
-
여러 쓰레드가 작동하기 때문에 이름이 Parallel이다.
-
그렇기 때문에 Serial GC 보다 빠르게 객체를 처리할 수 있다.
-
Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하다.
-
Parallel GC는 Throughput GC라고도 부른다.
-
다음 그림은 Serial GC와 Parallel GC의 쓰레드를 비교한 그림이다.

CMS(Concurrent Mark & Sweep) GC (-XX:+UseConcMarkSweepGC)
-
어떻게 하면 Full GC의 stop-the-world 상태를 줄일 수 있을까?라는 고민에서 출발한 GC.
-
Parallel Collector 와 같이 멀티 쓰레드로 Minor GC를 한다.
-
그리고 이 순간에는 stop-the-world가 발동한다.
-
하지만 Full GC는 거의 stop-the-world가 발생하지 않는다.
-
어플리케이션이 작동하는 중에 백그라운드에서 쓰레드를 만들어서
Old Generation 영역에 참조되지 있지 않은 객체들을 지속적으로 제거한다. -
즉 CMS의 장점은 stop-the-world가 거의 발생하지 않는다는 점이다.
-
정리하자면 백그라운드에서 항상 일을 하기 때문에
Minor GC에서 잠깐씩, 그리고 Full GC에서는 거의 발생하지 않는다. -
다음 그림은 Serial GC와 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 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재한다.
-
일단 백그라운드에서 항상 GC 쓰레드가 돌아야하기 때문에
다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다. -
Compaction(압축) 단계가 기본적으로 제공되지 않는다.
-
중간 중간에 Old Generation에 있는 객체들을 쏙쏙 잡아먹으면 메모리가 불규칙적으로 비어지게 된다.
-
즉 메모리 파편화가 발생한다.
-
만약 CPU 리소스가 부족해진다거나 메모리 파편화가 너무 심해서 메모리 공간이 부족해지면
Serial GC가 하던 방식(싱글 쓰레드로 청소)을 똑같이 따라하게 된다. -
따라서 CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 한다.
G1(Garbage First) GC
-
G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다.
-
다음 그림처럼 G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다.
-
그러다 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.
-
즉 지금까지 설명한 Young의 세가지 영역에서
객체가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다. -
G1 GC는 장기적으로 말도 많고 탈도 많은 CMS GC를 대체하기 위해서 만들어졌다.

-
G1 GC의 가장 큰 장점은 성능이다.
-
지금까지 설명한 어떤 GC 방식보다도 빠르다.
-
JDK 7에서 정식으로 G1 GC를 포함하여 제공한다.
+
-
힙 영역이 매우 큰 머신(최소 4GB)에서 돌리기에 적합한 GC이다.
-
CMS의 단점을 어느 정도 극복했다.
-
힙에 영역(Region)이라는 개념을 도입하였다.
-
힙을 여러 개의 Region으로 나눈다.
일부 Region은 Young Generation 영역으로
나머지 일부 Region은 Old Generation 영역으로 사용한다. -
Young Generation 영역을 정리하는 건 Parallel이나 CMS처럼 멀티쓰레드로 정리를 한다.
-
그리고 Old Generation 영역에 해당하는 여러개의 Region에 대해선
CMS처럼 백그라운드 쓰레드로 이 영역들을 정리를 한다. -
그런데 CMS와 차이점은 중간 중간 쓸모없는 객체들을 정리하는게 아닌
한 Region을 통째로 정리해버린다. -
참조가 없는 객체들은 지우고 사용 중인 객체는 다른 Region으로 고스란히 복사를 한다.
포인터 추적 기법 중 객체 이동 기법을 참고하자 -
다른 Region으로 복사하는 과정에서 차곡차곡 옮기므로
Compacting(압축)이 되므로 메모리 파편화 현상이 생기지 않게 된다. -
CMS의 문제점이었던
1. 많은 CPU 리소스 사용
2. 메모리 파편화 발생
2가지 문제 중 메모리 파편화 문제를 해결하게 된다.




近期评论