JVM 시리즈 - 가비지 컬렉션의 원리
가비지 컬렉션의 원리
가비지 컬렉션이 처리해야 하는 문제는 3가지다.
- 어떤 메모리를 회술할 것인가
- 언제 회수할 것인가
- 어떻게 회수할 것인가
높은 동시성을 달성하는데 가비지 컬렉션이 방해가 되는 상황이 온다면, 자동화된 기술을 적절히 모니터링하고 조율할 수 있어야 한다.
참조 카운팅 알고리즘
객체가 살아 있는지 판단하기 위해서 사용할 수 있다. 참조하는 곳에 따라서 카운터를 증감시키는 방식이다. 하지만 JVM 에서는 참조 카운팅을 사용하지 않는다. 참조 카운팅으로는 순환 참조 문제를 풀 수 없기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 두 객체 생성
GCObject objA=new GCObject();
GCObject objB=new GCObject();
// 내부 필드로 서로를 참조
objA.instance=objB;
objB.instance=objA;
// 참조 해제
objA=null;
objB=null;
// gc 수행
System.gc();
objA, objB 는 메모리에서 회수될 것이다. 따라서 자바 가상 머시는 객체 생사 판단에 참조 카운팅 알고리즘을 사용하지 않았음을 알 수 있다.
도달 가능성 분석 알고리즘
오늘날의 주류 프로그래밍 언어들은 모두 객체 생사 판단에 도달 가능성 분석 알고리즘을 이용한다. 이는 GC 루트라고 하는 루트 객체들을 시작 노드 집합으로 쓰는 것이다. 시작 노드에서 출발하여 참조하는 다른 객체들로 탐색해 들어가는 방식이다. 즉, GC 루트에 도달할 수 있는 참조 체인이 없는 객체는 더 이상 사용할 수 없다는 것이고, 이는 회수 대상이라는 뜻이다.
자바에서 GC 루트로 이용할 수 있는 객체는 정해져 있다.
- 가상 머신 스택에서 참조하는 객체
- 메서드 영역에서 클래스가 정적 필드로 참조하는 객체
- 메서드 영역에서 상수로 참조되는 객체
- 네이티브 메서드 스택에서 JNI 가 참조하느 객체
- 자바 가상 머신 내부에서 쓰이는 참조 객체
이상의 정해진 GC 루트들 외에도 가비지 컬렉터 종류에 따라서 다른 객체들도 임시로 추가될 수 있다. 이렇게 해서 전체 GC 루트 집합을 만들 수 있다.
참조
객체의 생사 판단으로 참조를 활용할 수 있다는 점은 이해했을 것이다. 그런데 객체의 상태는 ‘참조됐다’와 ‘참조되지 않았다’ 이렇게 두 가지뿐이다. 즉, ‘버리기는 아까운’ 객체를 표현할 수 없다. 이를 표현하기 위해서 JDK 1.2 부터 참조 개념이 확장됐다.
- 강한 참조 : 가장 전통적인 정의의 참조를 뜻한다. 이 객체는 가비지 컬렉터가 절대 회수하지 않는다.
- 부드러운 참조 : 유용하지만 필수는 아닌 객체를 표현하며, 메모리 오버플로가 나기 전에 회수 목록에 추가된다.
- 약한 참조 : 약한 참조는 다음번 가비지 컬렉션까지만 살아 있다.
- 유령 참조 : 객체 수명에 아무런 영향을 주지 않는다. 이는 대상 객체가 회수될 때 알림을 받기 위해서만 사용된다.
두 번의 표시 과정
도달 가능성 분석 알고리즘을 통해서 ‘도달 불가능’ 판단된 객체라고 반드시 죽는 것은 아니다. 참조 체인을 찾지 못한 객체는 첫번째 표시가 이루어지고, 필터링이 진행된다. 필터링 조건은 finalize() 메서드를 실행해야 하는지에 대한 여부이다.
finalize 가 필요 없는 객체이거나 가상 머신이 finalize 를 이미 호출한 경우, 모두 ‘실행할 필요 없음’으로 처리한다. finalize 를 실행해야 하는 객체는 F-queue 에 추가되고, 나중에 우선 순위가 낮은 스레드에 의해서 finalize 메서드를 실행한다.
이 메서드의 실행을 스레드가 기다리는 것은 아닌데, 이로 인해서 가비지 컬렉션 시스템 전체를 비정상 종료시킬 수 있기 때문이다. finalize 를 이용해서 객체는 부활할 수 있으며, 이 때 빠져나오지 못한다면 두 번째 표시 과정에서 회수될 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize 메서드 실행");
FinalizeEscapeGC.SAVE_HOOK = this;
}
}
public static void main(String[] args) {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc(); // 이 경우에는 finalize 메서드의 호출로 살아남을 것이다.
SAVE_HOOK = null;
System.gc(); // 이 경우에는 살아남을 수 없다. finalize 메서드를 호출해주는 것은 한번 뿐이다.
}