자바에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 Garbage Collector가 더 이상 필요 없는 객체를 찾아 지우는 작업을 한다.
Garbage Collector는 두 가지 가설(=weak generational hypothesis) 하에 만들어짐
Young 영역은 3개의 영역으로 나뉜다.
각 영역의 처리 절차를 순서에 따라서 기술하면 다음과 같다
Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 시스템이 정상적인 상황이 아니라고 보면 됨.
🔥 Eden 영역에 최초로 객체가 만들어지고, Survivor 영역을 통해서 Old 영역으로 오래 살아남은 객체가 이동한다는 사실은 꼭 기억하기 🔥
Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행함.
GC 방식에 따라서 처리 절차가 달라짐. 그건 밑에서 언급함
= Method Area
객체나 억류된 문자열 정보를 저장하는 곳이며 Old 영역에서 살아남은 객체가 영원히 남아 있는 곳은 아니다. 이 영역에서 GC가 발생할 수도 있는데 여기서 GC가 발생해도 Major GC의 횟수에 포함됨
매우 많은 객체가 Young 영역에 생성되었다가 사라지고 이 영역에서 객체가 사라질 때 Minor GC가 발생한다고 말함.
대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다.
Old 영역에 있는 모든 객체들을 검사하여 참조되지 않은 객체들을 한꺼번에 삭제한다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다는 GC가 적게 발생함. 시간이 오래 걸리며 실행 중 프로세스가 정지된다.
🔥 Stop-the-world 🔥
GC를 실행하기 위해 JVM이 어플리케이션 실행을 멈추는 것
stop-the-world가 발생하면 GC를 실행하는 스레드를 제외한 나머지 스레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에나 중단했던 작업을 다시 시작함
어떤 GC 알고리즘을 사용하더라도 stop-the-world는 발생하며, 대개 GC 튜닝이란 stop-the-world 시간을 줄이는 것을 말함
이러한 경우를 처리하기 위해 Old 영역에는 512바이트의 덩어리로 되어 있는 카드 테이블이 존재한다.
카드 테이블에는 old 영역에 있는 객체가 young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드 테이블만 뒤져서 GC 대상인지 식별한다.
카드 테이블은 Minor GC를 빠르게 할 수 있도록 하는 장치인 write barrier를 사용하여 관리함. write barrier 때문에 약간의 오버헤드는 발생하지만 전반적인 GC 시간은 줄어든다.
알고리즘에 따라 동작 방식이 매우 다양하지만 공통적인 원리가 있음
Garbage Collector는 힙 내의 객체 중에서 Garbage를 찾아내고 찾아낸 Garbage를 처리해서 힙의 메모리를 회수함
✔️ 참조되고 있지 않은 객체 = Garbage
✔️ 객체가 Garbage인지 아닌지 판단하는 개념 = Reachability
어떤 힙 영역에 할당된 객체가 유효한 참조가 ⭕️ : Reachability / ❌ : Unreachability
✔️ 하나의 객체는 다른 객체를 참조하고, 다른 객체는 또 다른 객체를 참조할 수 있기 때문에 참조 사슬이 형성 , 최초로 참조한 것을 Root Set
💡 힙 영역에 있는 객체들은 총 4가지 경우에 참조를 하게 됨
1. 힙 내의 다른 객체에 의한 참조
2. 자바 스택, 즉 자바 메소드 실행 시에 사용하는 지역변수와 파라미터들에 의한 참조
3. 네이티브 스택에 의해 생성된 객체에 대한 참조
4. 메소드 영역의 정적 변수에 의한 참조
-> 2,3,4번은 Root Set임. 즉, 참조 사슬 중 최초로 참조한 것
빈번한 GC의 실행은 시스템에 부담이 될 수 이씩에 성능에 영향을 미치지 않도록 GC 실행 타이밍은 별도의 알고리즘을 기반으로 계산하고 이 계산결과를 기반으로 GC 수행
JDK 7을 기준으로 다섯가지가 있음
1. Serial GC
적은 메모리와 CPU 코어 개수가 적을 때(1개) 적합한 방식. 운영 서버에서는 절대 사용 ❌
Mark-Sweep-Compact 알고리즘을 사용함
Mark : Old 영역에 살아 있는 객체를 식별 -> Sweep : 힙의 앞 부분부터 확인하여 살아 있는 것만 남김 -> Compaction : 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눔
2. Parallel GC
기본적인 GC 알고리즘은 Serial GC와 동일하지만 Parallel GC는 GC를 처리하는 스레드가 여러 개라서 보다 빠르게 GC 수행 가능. 메모리가 충분하고 코어의 개수가 많을 때 유리
3. Parallel Old GC
JDK 5 update 6부터 제공한 GC방식
Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다름
이 방식은 Mark-Summary-Compaction 알고리즘을 사용함
✔️ Mark-Sweep-Compact와 차이점 : GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별한다는 점에서 Sweep 단계와 다르고, 약간 더 복잡한 단계를 거침
4. Concurrent Mark & Sweep GC
✔️ 특징 : stop-the-world 시간이 매우 짧음, 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용함
✔️ 단점 : 다른 GC 보다 메모리와 CPU를 더 많이 사용한다. Compaction 단계가 기본적으로 제공되지 않는다
따라서, CMS GC를 사용할 때는 신중히 검토한 후에 사용해야 한다. 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 stop-the-world 시간이 길기 때문에 compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 함
5. G1(Garbage First) GC
지금까지의 Young 영역과 Old 영역에 대해서는 잊는게 좋다 🤬
G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.
즉, Young의 세가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다.
✔️ 장점 : 성능
✔️ 하지만 JDK 6에서는 그냥 시험삼아 사용할 수만 있도록 한다. JDK 7에서 정식으로 제공함
출처 : https://asfirstalways.tistory.com/159
https://d2.naver.com/helloworld/1329