- 가비지 컬렉터의 수거 대상은 무슨 근거로 판단할까?
- 왜 heap 은 여러 영역으로 나눠져 있을까?
다음과 같은 궁금증이 생겨 해당 내용을 포스팅하게 되었습니다.
가비지 컬렉션이란?
힙영역에서 사용 중인 객체와 사용 중이지 않는 객체를 식별하고 사용하지 않는 객체를 수거하여 메모리를 관리하는 기법입니다.
가비지 컬렉션의 루트 - 모든 객체 트리에 근원
가비지란 사용하지않는 객체를 말한다.
사용하지 않는 객체란 무엇일까?
사용하지 않는 객체란 GC Root와 관계가 있다. 어떤 객체가 유효한 참조가 존재한다면 'Reachable'그렇지 않으면 'Unreachable'이라 하며 'Unreachable' 한 객체는 GC의 대상이 된다.
객체는 다른 여러 객체를 참조하고 그 객체들도 다른 객체들을 참조하므로 객체는 참조 트리를 이룬다. 참조 트리에서 유효한 참조인지 확인하기 위해서 항상 유효한 최초의 객체가 이를 'GC Root' 라 한다.
garbage 대상인지 어떻게 확인할까?
GC root 참조 유무로 판단한다. 다른 객체가 참조하고 있더라고 root 참조가 없으면 한 번에 가비지 컬렉팅 대상이 된다.
힙에 있는 객체들에 대한 참조는 4가지 종류 중 하나이다.
- 힙 내의 다른 객체에 의한 참조(순환참조)
- Java stack, Java 메서드 실행 시에 사용되는 지역 변수와 파라미터들에 의한 참조
- Native Stack , 즉 JNI (Java Native Interface)에 의해 생성된 객체에 대한 참조
- 메서드 영역의 정적 변수에 의한 참조
위의 4가지 중 순환참조(힙 내의 다른 객체에 의한 참조)는 외부에서 참조하는 것이 아니기에 reachable vs unreachable 상태를 판단짓는 용도로 사용되지 않습니다. 즉 Root Space 에서 참조되고 있느냐는 나머지 3가지 참조에 대해서 결정됩니다.
Mark and Sweep
mark and sweep 알고리즘은 객체들을 reachable 상태와 unreachable 상태로 구분해서 GC 동작 시 unreachable 상태의 객체들이 차지하고 있는 메모리를 회수합니다.
1. root space 부터 시작해 살아있는 모든 객체를 탐색하고 발견된 모든 객체를 살아있다고 mark 합니다.
2. 힙메모리내에 마킹되지 않는 객체들은 삭제(Sweep) 됩니다.
위의 그림을 보면 객체들이 Root Space (혹은 Root set)에 참조되어 있으면 reachable object라고 하고, 참조되고 있지 않다면 Unreachable objects라고 합니다.
참조되고 있다면 reference count 가 1 증가합니다.
- reachable 객체
- Root space 에서 참조되고 있는 개체
- reference count 가 0 이 아닌 객체
- unreachable 객체
- Root space 에서 참조되고 있는 객체
- reference count 가 0 이 객체
Stop the world
GC에 대해 이해하려면, Stop-the-world에 대해 이해해야 합니다. GC 가 동작하기 위해 GC를 제외한 스레드가 잠시 멈추는 Stop-the-world 가 발생합니다.
GC 가 동작함으로써 JVM 의 메모리를 확보할 수 있다는 장점도 있지만 다른 스레드들이 동작하지 못하게 돼 프로그램의 성능 저하는 준다는 것입니다. 그래서 stop-the world 시간이 gc 성능에 중요한 요소입니다.
Minor GC와 Major GC 모두 실행될 때에는 stop-the-world 발생하지만 Minor GC 는 상대적으로 작은 Young 영역에 대해 GC 가 발생하기 때문에 stop-the-world 시간이 매우 짧지만, Major GC(혹은 Full GC)의 경우에는 비교적 큰 Old 영역에 대해 GC 가 발생하기 때문에 GC 가 오랜 시간 동안 지속됩니다.
Stop the world 는 언제 발생할까?
GC 알고리즘에 따라 다르겠지만 주로 마킹과정에서 발생합니다.
gc 도중 쓰레드가 루트참조를 할 수 있기 때문에 마킹할 때 root 참조가 없다는 것을 확실히 판단하기 위해 stop the world 가 발생합니다.
그 외 과정은 주로 병렬로 처리됩니다.
힙 영역은 왜 세분화되어있을까?
jvm 영역은 GC 가 효율적으로 동작할 수 있게 힙영역이 세분화되어 있습니다. 크게 Young, Old로 나눌 수 있고, Young 영역에는 Eden, Survivor0 , Survivor01 영역으로 나뉩니다.
GC를 만들고 힙 영역을 세분화할 때는 weak generational hypothesis라는 가설을 토대로 만들어졌다고 합니다. 가설의 내용은 아래와 같습니다.
대부분의 객체는 금방 접근 불가능한 상태(unreachable)가 된다.
오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
그래서 JVM의 힙 영역을 세분화시킴으로써 GC의 효율을 극대화시킨 것입니다.
먼저 영 제너레이션은 전체 힙의 일부분일 뿐이므로 이와 같이 처리하면 전체힙을 처리하는 것보다 더 빠릅니다. 이건 애플리케이션 스레드가 전체 힙이 한 번에 처리되는 것보다 더 짧은 기간 동안만 중지된다는 것을 의미합니다. 즉 전체 힙이 가득 찰 때까지 JVM 이 GC를 수행하려고 대기할 때보다 애플리케이션 스레드가 더 자주 멈춘다는 의미이므로 여기서 트레이트 오프를 해야 합니다. 하지만 더 자주 중지되더라도 대체로 짧게 처리되는 편이 더 이익입니다.
그래서 Young 영역은 Old 영역의 사이즈보다 작게 유지한 채, 대부분의 객체들이 Young 영역에서 메모리가 회수되도록 구현되어 있습니다.
다만 몇몇 객체들은 Young 영역에서 계속 살아남아 Old 영역으로 복사되는데 이 과정을 aging 이라고 합니다.
- 부분의 GC 알고리즘에서는 age를 임계값으로 두고, 특정 age가 되면 Old 영역으로 복사됩니다.
그리고 Young 영역에서 발생한 GC는 Minor GC라 부르고, Old 영역에서 발생한 GC는 Major GC(Full GC)라고 부릅니다.
Stop-the-world는 언제 일어날까?
대부분의 GC 알고리즘에서 루트탐색단계에서 STW 가 발생합니다.
GC 가 객체참조를 추적하거나 메모리에 객체를 옮긴다면 애플리케이션 스레드가 그 객체를 사용하고 있지 않다는 사실을 분명히 해야 합니다. 이건 특히 GC로 인해 주변 객체가 이동하면서 해당 동작이 일어나는 동안 객체의 메모리상의 위치가 변경되고 그로 인해 그 객체가 접근할 수 있는 애플리케이션 스레드가 없을 때 적용됩니다.
Soft, Weak, Phantom Reference
java.lang.ref 는 soft reference 와 weak reference 를 클래스 형태로 제공한다.
예를 들어, java.lang.ref.WeakReference 클래스는 참조 대상인 객체를 캡슐화(encapsulate) 한 WeakRefernce 객체를 생성한다.
이렇게 생성도니 WeakReference 객체는 다른 객체와 달리 Java GC 가 특별하게 취급한다. 캡슐화된 내부 객체는 weak reference 에 의해 생성된다.
다음은 WeakReference 클래스가 객체를 생성하는 예이다.
WeakReference<Sample> wr = new WeakReference<Sample>( new Sample());
Sample ex = wr.get();
...
ex = null;
위 코드에서 마지막 줄에서 ex 참조에 null 을 대입하여 처음 생성한 Sample 객체는 오직 WeakReference 내부에서만 참조된다. 이 상태의 객체를 weakly reachable 객체라고 하는데, 이에 대한 자세한 내용은 뒤에서 다룬다.
Java 스펙에서는 SoftReference, WeakReference, PhantomReference 3가지 클래스에 의해 생성된 객체를 " reference object" 라고 부른다. 이는 흔히 strong reference로 표현되는 일반적인 참조나 다른 클래스이 객체와는 달리 3가지 Reference 클래스의 객체에서만 사용하는 용어이다.
Reference와 Reachability
원래 GC 대상 여부는 reachable 인가 unreachable 인가로만 구분하였고 이를 사용자 코드에서는 관여할수 없었다. 그러나 java.lang.ref 패키지를 이용하여 reachable 객체들은 strongly reachable, softly 있었다. 다시 말해 GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있게 되었다.
녹색으로 표시한 중간의 두 객체는 WeakReference 로만 참조된 weaklye reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC 가 동작할 때, unreachable 객체뿐만 아니라 weakly reachable 객체로 간주되어 메모리에서 회수된다. root set 으로부터 참조 사슬에 포함되어 있음에도 불구하고 GC 가 동작할 때 회수되므로, 참조는 가능하지만 반드시 항상 유효한 필요는 LRU 캐시와 같은 임시 객체들을 저장하는 구조를 쉽게 만들 수 있다.
위 그림에서 WeakRerence 객체 자체는 weakely reachable 객체가 아니라 strongly reachable 객체이다. 또한 그림에서 A 로 표시한 객체와 같이 WeakRefernce 에 의해 참조되고 있는 동시에 root set 에서 시작한 참조사실에 포함되는 경우에는 weakly reachable 객체가 아니라 strongly reachable 객체이다.
GC 가 동작하기 위해 어떤 객체를 weakly reachable 객체로 판명하면, GC 는 WeakReference 객체에 있는 weakly reachable 객체에 대한 참조를 null 로 설정한다. 이에 따라 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되고, 가비지로 판명된 다른 객체들과 함께 메모리 회수 대상이 된다.
Strong Reference
GC 가 절대 제거되지 않음
- StrongRef는 강한 참조이므로 GC가 실행되어도 객체가 제거되지 않습니다.
- GC 는 사옹되지 않는 (Reachable 하지 않은) 객체만 제거하기 때문입니다.
public static void main(String[] args) {
String strongRef = new String("Strong Reference"); // 강한 참조
System.gc(); // GC 실행 요청
System.out.println("After GC: " + strongRef);
}
Soft Reference
메모리가 부족할 때 제거
public static void main(String[] args) {
String strong = new String("Strong Refernce");
SoftReference<String> sotfRef = new SoftReference<>(strong);
// WeakReference<String> weakRef = new WeakReference<>(strong);
strong = null; // 강한 참조 제거
System.gc();
System.out.println(sotfRef.get() != null ? "Object stayed" : "Object removed");
System.out.println(sotfRef.get());
}
Weak Reference
GC 가 실행될 때 즉시 제거
public static void main(String[] args) {
String strong = new String("Strong Refernce");
// SoftReference<String> sotfRef = new SoftReference<>(strong);
WeakReference<String> weakRef = new WeakReference<>(strong);
strong = null; // 강한 참조 제거
System.gc();
System.out.println(weakRef.get() != null ? "Object stayed" : "Object removed");
System.out.println(weakRef.get());
참고자료
https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/
https://velog.io/@ddangle/Java-GC-Garbage-Collector% EC%97%90-%EB% 8C%80% ED%95% B4
'JAVA' 카테고리의 다른 글
[JAVA] 제네릭 (Generic) (0) | 2024.11.08 |
---|---|
[자바의 신2] 정리해봅시다 [2장~ 11장] (0) | 2024.11.07 |
[JAVA] Error 와 Exception (0) | 2024.11.01 |
[JAVA] staic 과 final (1) | 2024.10.29 |
== 와 equals 차이 , hashcode (0) | 2024.10.24 |