가비지 컬렉터는 뭘까? (정리)

maketheworldwise·2022년 2월 13일
0


이 글의 목적?

이전 포스팅에서 가비지 컬렉터가 무엇인지에 대해 정리했었다. 하지만 막상 글을 작성하고나니 뭔가 뒤죽박죽 섞인 느낌이 들어 필요한 개념들만 블로그를 참고하여 나열해서 정리해보고자 한다.

GC Reachability

GC에서 가장 중요한 개념중에 하나라고 생각한다. GC는 Root Set에서 시작하여 객체에 대한 모든 경로를 탐색하고 그 경로에 있는 참조 객체들을 조사하여 그 객체에 대한 Reachability를 결정한다. GC가 Reachabiltiy로 객체가 GC의 대상인지 아닌지를 판단을 하는데, 이 때 판단하는 기준을 Reachable, Unreachable 상태로 나누어 구분한다. (Unreachable 상태인 객체만 가비지로 간주하여 처리)

Root Set으로 부터 시작한 참조 사슬에 속한 객체들은 모두 Reachable 상태이고, Reachable 상태는 또 다시 세부적으로 나뉜다. 더 정확히 말한다면, java.lang.ref 패키지에 있는 Strongly Reachable, Softly Reachable, Weakly Reachable, Phantomly Reachable로 구별되어 GC가 발생했을 때의 동작을 다르게 지정할 수 있다. 즉, Reachable 상태를 더 세부적으로 나눔으로서 GC 대상 판별 여부를 판별하는 부분에 사용자 코드가 개입하여 더 효율적인 메모리를 관리할 수 있도록 만들 수 있다는 것이다.

  • UnReachable (UnReachable Object, GC의 대상)
  • Reachable
    • Strongly Reachable (Strongly Reachable Object)
    • Softly Reachable (Softly Reachable Object)
    • Weakly Reachable (Weakly Reachable Object)
    • Phantomly Reachable (Phantomly Reachable Object)

GC Reference

Strong, Soft, Weak, Phantom 순으로 뒤로 갈 수록 GC에 의해 제거될 우선순위가 높아진다. 이 순서대로 정리해보자.

Strong Reference

우리가 일반적으로 생성하는 객체들은 Strong Reference에 속한다. 이 Reachable을 가진 객체들은 Root Set과 참조 관계가 연결되어있다면 제거되지 않는다.

(💡 개발자가 임의로 객체에 null을 할당하거나 객체가 Unreachable 상태가 된다면, 메모리에서 해제가 된다.)

Soft Reachable, SoftReference

오직 SoftReference 객체로만 참조된 Soft Reachable Object는 힙에 있는 메모리의 크기와 해당 객체의 사용 빈도에 따라 GC 여부가 결정된다. 다시 말해, 메모리가 부족하다고 판단될 때 수거해가고, 객체가 자주 사용된다면 수거해가지 않는다는 것이다.

오라클에서는 Softly Reachable Object 객체의 GC를 조절하기 위해 JVM 옵션을 제공한다.

# 기본값은 1000
-XX:SoftRefLRUPolicyMsPerMB=<N>

# Softly Reachable Object GC 여부 조건
[마지막 Strong Reference가 GC 된 때로부터 지금까지의 시간] > [옵션 설정값(N)] * [힙에 남은 메모리 크기]

# N = 1000, 힙에 남은 메모리 크기 = 100MB일 때의 GC 여부 조건
1000ms/MB * 100MB 
= 100000ms 
= 100sec 
= Softly Reachable Object가 100초 이상 사용되지 않으면 GC 회수 대상이 됨

Soft Reference는 GC 동작시 메모리가 부족할 경우 회수 대상이 되므로, 캐싱에 사용하게 되면 다른 비즈니스 로직에 필요한 다수의 메모리를 캐싱에서 사용하게 되어 잦은 GC가 발생해 성능상 이슈가 생길 수 있다.

(💡 Soft Reachable Object는 Weakly Reachable Object에 비해 더 오래 살아남는다.)

Weak Reachable, WeakReference

Weakly Reachable Object는 GC를 수행할 때마다 회수 대상이 된다. LRU 캐시와 같은 애플리케이션에서는 Softly Reachable Object 보다는 Weakly Reachable Object가 유리하므로 LRU 캐시를 구현할 때는 대체로 WeakReference를 사용한다. Softly Reachable Object는 힙에 남아있는 메모리가 많을 수록 회수 가능성이 낮기 때문에 다른 비즈니스 로직 객체들을 위해 어느 정도 비워두어야 할 힙 공간이 Softly Reachable Object에 의해 일정 부분 점유된다. 따라서 전체 메모리 사용량이 높아지고 GC가 더 자주 일어나며, GC에 걸리는 시간도 상대적으로 길어지는 문제가 있다.

LRU(Least Recently Used) 알고리즘으로 캐시에서 메모리를 다루기 위해 사용되는 알고리즘이다. 캐시가 사용하는 리소스의 양은 제한되어있고, 캐시는 제한된 리소스 내에서 데이터를 빠르게 저장하고 접근할 수 있어야 한다. 이를 위해 LRU 알고리즘은 메모리상에서 가장 최근에 사용된 적이 없는 캐시의 메모리부터 대체하여 새로운 데이터로 갱신시켜준다.

Reference Queue

SoftReference와 WeakReference 객체가 참조하는 객체가 GC 대상이 되면 각 객체 내부의 참조는 null로 설정되고, 객체 자체는 Reference Queue에 enqueue 된다. Reference Queue에 enqueue하는 작업은 GC에 의해 자동으로 수행된다.

Reference Queue의 poll(), remove() 메소드를 통해 Reference Object가 enqueue 되었는지 확인과 동시에 GC되었는지 파악할 수 있고, 관련된 리소스나 객체에 대해 후처리 작업을 할 수 있다. 즉, 어떤 객체가 더 이상 필요 없게 되었을 때 관련된 후처리 작업을 해야하는 애플리케이션에서는 Reference Queue를 유용하게 사용할 수 있다. 컬렉션 클래스중에서 간단한 캐시를 구현하는 용도로 자주 사용하는 WeakHashMap 클래스는 Reference Queue와 WeakReference를 사용하여 구현되어있다.

갑자기 왜 Reference Queue에 대해 알아야할까? SoftReference와 WeakReference는 Reference Queue를 인자로 받는 생성자와 받지 않는 생성자가 존재하지만, PhantomReference는 Reference Queue를 인자로 받는 하나의 생성자만 존재하기에 반드시 Reference Queue를 사용해야하기 때문이다.

SoftReference, WeakReference 공통점

우선, Soft Reachable Object, Weakly Reachable Object, Unreachable Object 모두 - GC가 GC 대상인 객체를 찾는 작업과 GC 대상인 객체를 처리하여 메모리를 회수하는 작업은 연속적으로 이루어지지 않는다. 따라서 GC가 실제로 언제 객체를 회수할지에 대해서는 GC 알고리즘마다 달라 GC가 수행될 때마다 반드시 메모리까지 회수된다는 보장이 없다는 공통점을 가지고 있다.

이 소제목의 주제에 맞게 돌아와 앞서 정리한 내용을 토대로 SoftReference와 WeakReference의 공통점을 찾아보자.

  • 각 Softly Reachable Object, Weakly Reachable Object가 GC하기로 결정되면, 참조 사슬에 존재하는 SoftReference, WeakReference 내의 참조가 null로 설정되고 Unreachable Object가 되어 가비지 객체가 된다.
  • 캐시 용도로 사용할 수 있다. (Soft Reachable Object보다는 Weak Reachable Object가 더 적절 😉)
  • Reference Queue를 인자로 받는 생성자가 존재한다.

Phantom Reachable, Phantom Reference

Phantom Reachable은 파이널라이즈 작업이 이루어진 후에 GC 알고리즘에 따라 할당된 메모리를 회수한다. 즉, 파이널라이즈와 메모리 회수 사이에 관여를 하게 되는데, Strongly Reachable, Softly Reachable, Weakly Reachable에 해당하지 않고 PhantomReference로만 참조되는 객체는 파이널라이즈된 후에 Phantomly Reachable로 간주된다.

  1. Soft References
  2. Weak References
  3. 파이널라이즈
  4. Phantom References
  5. 메모리 회수

어떤 객체에 GC 여부를 판별하는 작업은 이 객체의 Reachabiltiy를 Strong, Soft, Weak 순으로 먼저 판별하고 모두 아닐 경우 파이널라이즈를 진행한다. 그리고 대상 객체를 참조하는 PhantomReference가 있을 경우 Phantomly Reachable로 간주하여 PhantomReference를 Reference Queue에 넣고 파이널라이즈 이후 작업을 애플리케이션이 수행하게 하고 메모리 회수는 지연시킨다.

PhantomReference의 get() 메소드는 항상 null을 반환하기 때문에 한번 Phantomly Reachable로 판명된 객체는 더 이상 사용될 수 없게 된다. 또한 Phantomly Reachable로 판명된 객체에 대한 참조를 GC가 자동으로 null로 설정하지 않으므로, 후처리 작업 후에 사용자 코드에서 명시적으로 clear() 메소드를 실행하여 null로 설정해야 메모리 회수가 진행된다.

(💡 PhantomReference는 finalize() 메소드 대용으로 사용하는데, 객체가 부활할 수 있기 때문에 더 안전하다는 특징을 가지고 있다고 기술된 블로그를 참고하면 좋을 것 같다. 🤔)

정리해보자

  • GC는 GC 대상 객체를 찾고, 대상 객체를 처리(파이널라이즈)하고 할당된 메모리를 회수하는 작업으로 구성된다.
  • 애플리케이션은 사용자 코드에서 객체의 Reachability를 조절하여 GC에 일부 관여할 수 있다.
  • 객체의 Reachability를 조절하기 위해 java.lang.ref 패키지의 클래스를 사용한다.

대부분 내부 캐시를 구현하고자 하는 대부분의 애플리케이션은 WeakReference, WeakHashMap만으로 충분하다고 한다. 종종 SoftReference를 사용하기도 하지만, PhantomReference는 거의 사용하지 않는다고 한다.

힙 메모리 구조

간단하게만 정리하자.

  • Young 영역
    • 새로운 객체는 Eden에 생성된다.
    • Eden 영역에 생성된 객체가 가득차면 GC가 발생한다.
    • GC가 발생하고도 살아남은 객체들은 Survivor0 영역으로 이동한다. (반복)
    • Eden 영역과 Survivor0 영역이 모두 가득 차면 GC가 발생하고, 살아남은 객체들은 모두 Survivor1 영역으로 이동한다. (반복)
  • Old 영역
    • Survivor 영역으로 이동하는 과정이 반복될 때, 일정 시간이나 횟수 이상 살아남은 객체들은 Old 영역으로 이동한다.

정확히 이해했다고는 말은 할 수가 없을 것 같다. 하지만 어느 정도 개념의 틀을 잡은 정도로 만족하고 나중에 다시 정리해보자. 😅

이 글의 레퍼런스

profile
세상을 현명하게 이끌어갈 나의 성장 일기 📓

0개의 댓글