가비지 컬렉션은 더 이상 사용되지 않는 메모리를 자동으로 찾아내어 해제하는 메모리 관리 메커니즘이다. Java 개발에서 가장 중요한 특징 중 하나로, 개발자가 메모리 관리에 대한 부담을 덜 수 있게 해준다.
가비지(Garbage)란?
프로그램을 개발하고 실행하는 과정에서는 더 이상 사용되지 않는 메모리인 '가비지(Garbage)'가 발생한다. 이는 다음과 같은 경우에 발생할 수 있다.
// 1. 객체의 참조가 null이 되는 경우
String text = "Hello";
text = null; // 이전의 "Hello" 객체는 가비지가 됨
// 2. 객체의 참조 범위를 벗어나는 경우
public void createObject() {
String localVar = "Hello"; // 메서드 종료 시 접근 불가능
} // localVar는 메서드 종료와 함께 가비지가 됨
// 3. 객체의 참조가 다른 객체를 가리키는 경우
String a = "Hello";
a = "World"; // "Hello" 객체는 가비지가 됨
가비지 컬렉션 대상
가비지 컬렉션은 특정 객체가 garbage인지 아닌지 판단하기 위해서 도달성, 도달능력(Reachability) 이라는 개념을 적용한다. 객체에 레퍼런스가 있다면 Reachable로 구분되고, 객체에 유효한 레퍼런스가 없다면 Unreachable로 구분해버리고 수거해버린다.
- Reachable : 객체가 참조되고 있는 상태
- Unreachable : 객체가 참조되고 있지 않은 상태 (GC의 대상이 됨)
예를들어 JVM 메모리에서는 객체들은 실질적으로 Heap영역에서 생성되고 Method Area이나 Stack Area 에서는 Heap Area에 생성된 객체의 주소만 참조하는 형식으로 구성된다.
하지만 이렇게 생성된 Heap Area의 객체들이 메서드가 끝나는 등의 특정 이벤트들로 인하여 Heap Area 객체의 메모리 주소를 가지고 있는 참조 변수가 삭제되는 현상이 발생하면, 위의 그림에서의 빨간색 객체와 같이 Heap영역에서 어디서든 참조하고 있지 않은 객체(Unreachable)들이 발생하게 된다.
이러한 객체들을 주기적으로 가비지 컬렉터가 제거해주는 것이다.
Minor GC와 Major GC
jvm의 힙 영역은 처음 설계될 때 다음의 2가지를 전제로 설계되었다.
- 대부분의 객체는 금방 접근 불가능한 상태(unreachable)가 된다.
- 오래된 객체에서 새로운 객체로의 참조는 아주 적게 존재한다.
즉, 객체는 대부분 일회성이 되며 메모리에 오랫동안 남아있는 경우는 드물다는 것. 그렇기 때문에 객체의 생존 기간에 따라 물리적인 힙 영역을 나누게 되었고 yong, old 2가지 영역으로 설계되었다.
- young
- 새롭게 생성된 객체가 allocation되는 영역
- 대부분의 객체가 금방 unreachable 상태가 되기 때문에, 많은 객체가 young 영역에 생성되었다가 사라진다.
- young 영역에 대한 가비지 컬렉션을 minor gc라고 한다.
- old
- young 영역에서 reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 가비지는 적게 발생한다.
- old 영역에 대한 가비지 컬렉션을 major gc라고 한다.
old 영역이 young 영역보다 크게 할당되는 이유는 young 영역의 수명이 짧은 객체들은 큰 공간을 필요로 하지 않으며 큰객체들은 young 영역이 아니라 바로 old 영역으로 할당되기 때문이다.
예외적으로 old 영역에 있는 객체가 young 영역의 객체를 참조하는 경우도 존재한다. 이러한 경우를 대비하여 old 영역에는 512 bytes의 덩어리(chunk)로 되어있는 카드 테이블이 존재한다.
카드 테이블에는 old 영역에 있는 객체가 young 영역의 객체를 참조할 때 마다 그에 대한 정보가 표시된다. 카드 테이블이 도입된 이유는 간단하다. young 영역에서 가비지 컬렉션이 실행될 때 모든 old 영역에 존재하는 객체를 검사하여 참조되지 않는 young 영역의 객체를 식별하는 것이 비효율적이기 때문이다. 그렇기 때문에 young 영역에서 GC가 진행될 때 카드 테이블만 조회하여 GC의 대상인지 식별할 수 있도록 하고 있다.
GC의 동작 방식
young과 old는 서로 다른 메모리 구조이므로 세부적인 동작 방식을 다르다. 하지만 기본적으로 GC가 실행된다고 하면 다음의 2가지 공통적인 단계를 따른다.
- stop the world
- mark and sweep
stop the world
stop the world는 가비지 컬렉션을 실행하기 위해 jvm이 애플리케이션의 실행을 멈추는 작업이다. gc가 실행될 때는 gc를 실행하는 스레드를 제외한 모든 스레드들의 작업이 중단되고, gc가 완료되면 작업이 재개된다. 당연히 모든 스레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, gc의 성능 개선을 위해 튜닝을 한다고 하면 stop-the-world의 시간을 줄이는 작업을 한다. 또한 jvm에서도 이러한 문제를 해결하기 위해 다양한 실행 옵션을 제공한다.
mark and sweep
- mark: 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
- sweep: mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
stop the world를 통해 모든 작업을 중단시키면, gc는 스택의 모든 변수 또는 reachable 객체를 스캔하면서 각각이 어떤 객체를 참고하고 있는지 탐색하게 된다. 그리고 사용되고 있는 메모리를 식별하는데 이러한 과정을 mark라고 한다. 이후 mark가 되지 않는 객체들을 메모리에서 제거하는데 이러한 과정을 sweep이라고 한다.
Minor GC의 동작 과정
마이너 GC를 이해하기 위해 young 영역의 구조에 대해 이해해야 한다. young 영역은 1개의 eden 영역과 2개의 survivor 영역. 총 3가지로 나뉜다.
- eden : 새로 생성된 객체가 할당되는 지역
- survivor: 최소 1번 이상의 GC로부터 살아남은 객체가 존재하는 영역
객체가 새롭게 생성되면 young 영역 중에서도 eden에 할당이 되고, eden이 꽉 차면 마이너 GC가 발생한다. 사용되지 앟는 메모리는 해제되고 eden 영역에 존재하는 (사용중인) 객체는 survivor 영역으로 옮겨지게 된다. survivor 영역은 2개이지만 반드시 1개의 영역에만 존재하고, 1개는 비어있어야 한다.
- 대상 영역
- eden 영역과 from survivor 영역이 대상이다.
- 새로 생성된 대부분의 객체는 eden에 할당된다.
- Minor GC 발생 조건
- eden 영역이 가득 차면 마이너 GC가 트리거된다.
- 새로운 객체를 할당하려 할 때 eden 영역의 공간이 부족한 경우 발생한다.
- Mark 단계
- GC Root(스택 변수, 정적 변수 등)부터 시작하여 접근 가능한 객체를 식별한다.
- eden 영역과 from survivor 영역의 살아있는 객체를 마킹한다.
- copy/sweep 단계
- Eden 영역의 살아있는 객체를 To Survivor 영역으로 복사된다.
- From Survivor 영역의 살아있는 객체도 To Survivor 영역으로 복사된다.
- 복사될 때마다 객체의 age가 증가, 마킹되지 않은 객체는 자동으로 메모리가 해제된다.
- survivor 영역 교체
- to survivor가 from survivor가 되고, 이전의 from survivor사 to survivor가 된다.
- 다음 마이너 GC때는 새로운 from survivor 영역에서 to survivor 영역으로 복사가 진행된다.
- promotion 처리
- 객체의 age가 임게값에 도달하면 old 영역으로 promotion된다.
- survivor 영역의 크기를 초과하는 객체들도 바로 old 영역으로 promotion된다.
- 메모리 정리
- eden 영역과 from survivor 영역은 완전히 비워진다.
- to survivor영역에는 살아남은 객체들만 존재한다.
GC 완료 후
eden 영역은 비워지고, 살아남은 객체들은 to survivor로 이동했다. 각 객체의 age가 증가했으며, age가 임계값(기본값은 15정도)에 도달하면 old generation으로 promotion된다. 그리고 survivor의 from과 to의 역할이 교체된다. 이러한 과정이 반복되면서 Young Generation의 메모리가 관리되며, 오래 살아남은 객체들은 점진적으로 Old Generation으로 이동하게 된다.
Major GC 동작 과정
Young 영역에서 오래 살아남은 객체는 Old 영역으로 Promotion됨을 확인할 수 있었다. 그리고 Major GC는 객체들이 계속 Promotion되어 Old 영역의 메모리가 부족해지면 발생하게 된다. Young 영역은 일반적으로 Old 영역보다 크키가 작기 때문에 GC가 보통 0.5초에서 1초 사이에 끝난다. 그렇기 때문에 Minor GC는 애플리케이션에 크게 영향을 주지 않는다. 하지만 Old 영역은 Young 영역보다 크며 Young 영역을 참조할 수도 있다. 그렇기 때문에 Major GC는 일반적으로 Minor GC보다 시간이 오래걸리며, 10배 이상의 시간을 사용한다. 참고로 Young 영역과 Old 영역을 동시하 처리하는 GC는 Full GC라고 한다.
요약
참고 자료