참고 : https://d2.naver.com/helloworld/1329
Java Garbage Collection
Generational Garbage Collection
stop-the-world
: GC
를 실행하기 위해 JVM
이 애플리케이션 실행을 멈추는 것
즉, GC
를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC
작업을 완료한 후에야 중단했던 작업을 다시 시작한다. GC
튜닝이란 이 stop-the-world
시간을 줄이는 것이다.
Java
는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않는다. 그렇기에 Garbage Collector
가 더 이상 필요없는 객체를 찾아 지우는 작업을 한다. 이는 두 전제 조건이 있다.
- 대부분의 객체는 금방 접근 불가능 상태(
unreachable
)가 된다. - 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
이러한 전제 조건을 weak generational hypothesis
라고 한다. 이 전제 조건을 최대한 살리기 위해 HotSpot VM
에서 크게 2개로 물리적 공간을 나누었다. 이 둘로 나눈 공간이 Young 영역과 Old 영역이다.
- Young 영역(
Young Generation
영역): 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때Minor GC
가 발생한다고 말한다. - Old 영역(
Old Generation
영역): 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기에 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질때Major GC
가 발생한다고 말한다.
영역별 데이터 흐름을 그림으로 보면 다음과 같다.
picture 1
위 그림의 Permanent Generation
영역은 Method Area
라고도 한다. 객체나 억류된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아있는 곳은 절대 아니다. 이 영역에서 GC
가 발생할 수도 있는데, 여기서 GC
가 발생해도 Major GC
가 발생한다고 한다.
Old 영역에 있는 객체가 Young 영역의 객체를 참조하는 경우가 있을 때에는, 이러한 경우를 처리하기 위해 Old 영역에는 512바이트의 덩어리(chunk
)로 되어있는 카드 테이블
이 존재한다.
카드 테이블
에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC
를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC
대상인지 식별한다.
picture 2
Young 영역의 구성
GC
를 이해하기 위해서 객체가 제일 먼저 생성되는 Young 영역부터 보면, Young 영역은 3개의 영역으로 나뉜다.
- Eden 영역
- Survivor 영역 (2개)
각 영역의 처리 절차를 순서에 따라서 기술하면 다음과 같다.
- 새로 생성한 대부분의 객체는 Eden 영역에 위치한다.
- Eden 영역에서
GC가 한 번 발생한 경우
살아남은 객체는Survivor 영역 중 하나로 이동
한다. - Eden 영역에서
GC가 발생하면
이미 살아남은객체가 존재하는 Survivor 영역으로
객체가 계속 쌓인다. 하나의 Survivor 영역이 가득차게 되면
그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득찬 Survivor 영역은다시 비어있는 상태
로 된다.- 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.
이 절차를 확인해보면, Survivor 영역 중 하나는 반드시 비어있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나 사용량이 0이라면 시스템은 정상적인 상황이 아니라고 생각하면 된다.
이렇게 Minor GC
를 통해서 Old 영역까지 데이터가 쌓인 것을 간단히 나타내면 다음과 같다.
picture 3
참고로, HotSpot VM
에서는 보다 빠른 메모리 할당을 위해서 두 가지 기술을 사용한다.
bump-the-pointer
Thread-Local Allocation Buffers
bump-the-pointer
: Eden 영역에 할당된 마지막 객체를 추적한다. 마지막 객체는 Eden 영역의 맨 위에 있다. 그리고 그 다음에 생성되는 객체가 있으면 해당 객체의 크기가 Eden 영역에 넣기 적당한지 확인하여 적당하다고 판정되면 Eden 영역에 넣게되고, 새로 생성된 객체가 맨 위에 있게 된다. 즉, 새로운 객체를 생성할 때 마지막에 추가된 객체만 점검하면 되므로 빠르게 메모리 할당이 이루어진다.
멀티 스레드 환경을 고려하면 Thread-Safe
하기 위해서 만약 여러 스레드에서 사용하는 객체를 Eden 영역에 저장하려면 Lock이 발생할 수 밖에 없고, lock-contention
때문에 성능은 떨어지게 된다. 이를 해결한 것이 TLABs
이다.
Thread-Local Allocation Buffers
: 이는 각각의 스레드가 몫에 해당하는 Eden 영역의 작은 덩어리를 가질 수 있도록 하는 것이다. 각 스레드에는 자기가 갖고 있는 TLAB
에만 접근할 수 있기 때문에, bump-the-pointer
를 사용하더라도 아무런 Lock이 없이 메모리 할당이 가능하다.
Old 영역에 대한 GC
Old 영역은 데이터가 가득 차면 GC
를 실행한다. GC
방식에 따라서 처리절차가 달라지므로 어떤 GC
방식이 있는지 살펴보면 될 것이다. GC
방식은 JDK 7
을 기분으로 5가지 방식이 있다.
- Serial GC
- Parallel GC
- Parallel Old GC (Parallel Compacting GC)
- Concurrent Mark & Sweep GC (CMS)
- G1 (Garbage First) GC
Serial GC (-XX:+UseSerialGC)
Old 영역의 GC
는 mark-sweep-compact
라는 알고리즘을 사용한다. 이 알고리즘의 첫 단계는 Old 영역에 살아 있는 객체를 식별(mark
)하는 것이다. 그 다음은 힙(heap
)의 앞 부분부터 확인하여 살아있는 것만 남긴다(sweep
). 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(compaction
).
Serial GC
는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이다.
Paralle GC (-XX:+UseParallelGC)
Parallel GC
는 Serial GC
와 기본적인 알고리즘은 같다. 그러나 Serial GC
는 GC
를 처리하는 스레드가 하나인 것에 비해, Parallel GC
는 GC
를 처리하는 스레드가 여러개이다. 그렇기 때문에 Serial GC
보다 빠르게 객체를 처리할 수 있다. Parallel GC
는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Throughput GC
라고도 부른다.
picture 4
Parallel Old GC (-XX:+UseParallelOldGC)
Parallel Old GC
는 JDK 5 update 6
부터 제공한 GC
방식이다. 앞서 설명한 Parallel GC
와 비교하여 Old 영역의 GC
알고리즘만 다르다. 이 방식은 Mark-Summary-Compaction
단계를 거친다. Summary
단계는 앞서 GC
를 수행한 영역에 대해서 별도로 살아있는 객체를 식별한다는 점에서 Mark-Sweep-Compaction
알고리즘의 Sweep
단계와 다르며, 좀 더 복잡한 단계를 거친다.
CMS GC (-XX:+UseConcMarkSweepGC)
다음 그림은 Serial GC
와 CMS 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 방식보다 메모리와 CPU를 더 많이 사용한다.
- Compaction 단계가 기본적으로 제공되지 않는다.
따라서, CMS GC
를 사용할 때에는 신중히 검토한 후에 사용하여야 한다. 그리고 조각난 메모리가 많아 Compaction
작업을 실행하면 다른 GC
방식의 stop-the-world
시간보다 더 길기 때문에 Compaction
작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.
G1 GC
G1 GC
는 바둑판의 각 영역에 객체를 할당하고 GC
를 실행한다. 그러다가 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC
를 실행한다. 즉, 지금까지 설명한 Young의 3가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC
방식이라고 할 수 있다. G1 GC
는 장기적으로 문제가 많은 CMS GC
를 대체하기 위해 만들어졌다.
G1 GC
의 가장 큰 장점은 성능이다. 지금까지 설명한 GC
방식 중에 제일 빠르다. 하지만 JDK 6
에서는 G1 GC
를 early access
라고 부르며 시험삼아 사용할 수 있도록 한다. 그리고 JDK 7
에서 정식으로 G1 GC
를 포함하여 제공한다.