가비지컬렉터


Java의 가비지 컬렉터는 많은 종류가 있지만 크게 두가지 작업을 공통적으로 수행합니다.

  1. 힙 메모리 내의 객체 중 가비지를 찾아낸다.
  2. 찾아낸 가비지를 처리해서 힙 메모리를 회수한다.

최초의 Java 에서는 이들 가비지 컬렉션 작업에 사용자 코드가 관여 하지 않도록 구현되어 있었습니다. 좀 더 다양한 방법으로 객체를 처리하려는 요구가 있었고 이에 따라 JDK 1.2 부터 java.lang.ref 패키지를 추가해서 제한적이나마 사용자 코드와 GC가 상호작용 할 수 있게 했습니다.

 

java.lang.ref 패키지는 객체를 new로 생성했을 때 생기는 string reference 이외에도 soft, weak, phantom 3가지의 새로운 참조 방식을 각각의 Reference 클래스로 제공합니다. 이 3가지 Reference 클래스를 애플리케이션에 사용하면 앞서 설명하였듯이 GC에 일정 부분 관여할 수 있습니다.

 

 

가비지컬렉터와 Reachability


Java 가비지컬렉터는 객체가 가비지인지 판별하기 위해서 reachability 라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 reachable 로 간주하고 없으면 unreachable 로 간주한다.

 

GC는 unreachable 로 간주된 객체를 가비지로 판단하여 GC (Garbage Collection)을 수행합니다. 한 객체는 여러 다른 객체를 참조할 수 있고, 참조된 객체들도 또다른 객체를 참조할 수 있으므로 객체들의 참조는 사슬모양을 이룬다. 이런 상황에서 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.

 

JVM 에서 메모리 영역인 런타임 데이터 영역 (runtime data area) 의 구조를 그림으로 그리면 다음과 같다

 

런타임 데이터 영역은 위와 같이 크게 세 부분으로 나뉜다.

  • 스레드가 차지하는 영역
  • 객체를 생성 및 보관하는 힙
  • 클래스 정보가 차지하는 영역인 메서드 영역

(객체에 대한 참조는 화살표로 표시되어 있다.)

힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.

  • 힙 내의 다른 객체에 의한 참조 (1번)
  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

 

이 중 1번을 제외한 나머지가 root set 이고 reachability 를 판가름하는 기준이 된다.

reachability 를 더 자세히 설명하기 위해 root set과 힙 내의 객체를 중심으로 다시 그리면 다음과 같다.

 

위 그림에서 보듯, root set으로 부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고 이 참조사슬 밖에 있는 객체는 참조가 있다고 해도 unreachable 객체로 GC의 대상이다. 오른쪽 아래 객체처럼 reachable 객체를 참조해도 다른 reachable 객체가 이 객체를 참조하지 않는다면 이 객체는 unreachable 객체이다. 이 그림에서의 참조는 모두 java.lang.ref 패키지를 사용하지 않은 일반적인 참조이며, 이를 흔히 strong reference 라고 부른다.

 

 

Soft, Weak, Phantom Reference


java.lang.ref 는 soft reference 와 weak reference, phantom reference 를 클래스 형태로 제공한다. 예를 들어서 약한 참조를 생성하기 위해서 사용하는 클래스인 java.lang.ref.WeakReference 클래스는 참조 대상이 될 객체를 캡슐화(encapsulate)한다. Java 가비지 콜렉터는 이 객체를 일반적인 객체와 다르게 특별하게 취급한다. (자세한 설명은 뒤에서). 캡슐화된 내부 객체는 weak reference 에 의해서 참조된다.

 

다음은 WeakReference 클래스가 객체를 생성하는 예제이다.

WeakReference<Sample> wr = new WeakReference<Sample>( new Sample());  
Sample ex = wr.get();  
...
ex = null;

위 코드의 첫줄의 WeakReference 객체는 Sample 객체를 캡슐화한 객체이다. 이렇게 생성된 Sample 객체는 두 번째 줄에서 get() 메서드를 통해서 다른 참조에 대입된다. 이 시점에서는 WeakReference 객체 내의 참조와 ex 변수의 참조, 두 개의 참조가 처음 생성한 Sample 객체를 가리킨다.

 

위 코드 마지막 줄에서 ex 변수에 null 을 대입하면 처음 생성한 Sample 객체는 오직 WeakReference 내부에서만 참조된다. 이 상태를 weakly reachable 객체라고 하는데 자세한 내용은 뒤에서 다룬다.

 

Java 스펙에서는 SoftReference, WeakReference, PhantomReference 3가지 클래스에 의해 생성된 객체를 reference object 라고 부른다. 그리고 이들 reference object 에 의해 참조된 객체는 “referent” 라고 부른다.

위 소스 코드에서 new WeakReference() 생성자로 의해 생성된 wr 객체는 reference object 이고 new Sample() 생성자로 생성된 객체는 referent 이다.

 

 

Reference와 Reachability


앞에서 설명한 것처럼, 원래 GC 대상 여부는 reachable 인가 unreachable 인가로만 구분하였고, 이를 사용자 코드에서 관여할 수 없었다. 그러나 java.lang.ref 패키지를 이용해서 reachable 객체들을 strongly, softly, weakly, phantomly 4가지 상태로 더 자세히 구별해서 GC의 동작을 달리 지정할 수 있게 되었다. 즉, GC 대상 여부를 판별하는 부분에 사용자 코드가 개입할 수 있게 되었다.

 

녹색으로 표시한 중간의 두 객체는 WeakReference 로만 참조된 weakly reachable 객체이고, 파란색 객체는 strongly reachable 객체이다. GC 가 동작할 때 unreachable 객체 뿐 아니라, weakly reachable 객체도 가비지 객체로 간주되어 메모리에서 회수된다. (참조가 끊어지기 때문에) root set 으로 부터 시작된 참조 사슬에 있음에도 불구하고 GC가 동작할 때 회수 된다.

 

위 그림에서 WeaklyReference 객체 자체는 weakly reachable 객체가 아니라 strongly reachable 객체이다. A의 경우에는 WeakReference 에 의한 참조 이외에 Root set으로 부터 시작된 참조 사슬에 포함되어 있기 때문에 weakly reachable 객체가 아니라 strongly reachable 객체이다.

 

GC가 동작하여 어떤 객체를 weakly reachable 객체로 판명하면 GC가 WeakReference 객체의 weakly reachable 객체에 대한 참조를 null 로 설정한다. 그래서 weakly reachable 객체는 unreachable 객체와 마찬가지 상태가 되어 가비지로 판명이 나는 것이다.

 

 

Softly Reachable과 SoftReference


softly reachable 객체, 즉 strong reachable 이 아니고 오직 SoftReference 객체로만 참조된 객체는 힙에 남아있는 메모리의 크기와 해당 객체의 사용 빈도에 따라서 GC 여부가 결정된다.

 

그래서 reachable 객체는 weakly reachable 객체와 달리 GC가 동작할 때마다 회수되는 것이 아니고, 자주 사용할 수록 더 오래 살아남는다. Oracle HotSpot VM 에서는 softly reachable 객체의 GC를 조절하기 위해 다음 JVM 옵션을 제공한다.

-XX:SoftRefLRUPolicyMSPerMB=<N>

이 옵션의 기본 값을 1000이다.

 

softly reachable 객체의 GC 여부는 위 옵션 값으로 다음 수식을 계산한 결과에 의해 결정된다.

(마지막 strong reference 가 GC 된 때 부터 지금까지의 시간) > (옵션 설정값 N) * (힙에 남아있는 메모리 크기)

어떤 객체가 사용된다는 것은 strong reference 에 의해 참조되는 것이고 위 수식의 좌변은 해당 객체가 얼마나 자주 사용되는지를 의미한다. 만약 N 이 1000이고 남은 힙 메모리 크키가 100MB 이면 수식의 우변은 1000 * 100 = 100,000ms = 100초가 된다. 즉 reachable 객체가 100초 이상 사용되지 않으면 GC 에 의해 회수 대상이 된다. 힙에 남아있는 메모리가 작을수록 우변의 값이 작아지므로, 힙이 거의 소진되면 대부분의 softly reachable 객체는 모두 메모리에서 회수되어 OutOfMemoryError 를 막게 된다.

 

softly reachable 객체를 GC하기로 결정나면 WeakReference 의 경우와 마찬가지로 SoftReference 객체의 softly reachable 객체에 대한 참조를 null 로 설정한다. 이 후 softly reachable 객체는 unreachable 객체가 되어 GC에 의해 메모리에서 회수된다.

 

 

ReferenceQueue


phantomly reachable 객체의 동작과 PhantomReference를 설명하기 전에 java.lang.ref 패키지에서 제공하는 ReferenceQueue 클래스에 대해 설명할 필요가 있다.

 

SoftReference 객체나 WeakReference 객체가 참조하는 객체가 GC 대상이 되면 SoftReference 객체, WeakReference 객체 내의 참조는 null로 설정되고 SoftReference 객체, WeakReference 객체 자체는 ReferenceQueue에 enqueue된다.

 

ReferenceQueue에 enqueue하는 작업은 GC에 의해 자동으로 수행된다. ReferenceQueue의 poll() 메서드를 활용해서 reference object 를 꺼내면 softly reachable 객체나 weakly reachable 객체가 GC되었는지를 파악할 수 있고 이와 관련한 후처리 작업을 할 수 있다.

 

실제 Java Collections 클래스 중에서 간단한 캐시를 구현하는 용도로 자주 사용되는 WeakHashMap 클래스는 ReferenceQueue 와 WeakReference 를 사용하여 구현되어 있다.

 

SoftReference 와 WeakReference 는 ReferenceQueue 를 사용할 수도 있고 사용하지 않을 수도 있지만 PhantomReference 는 반드시 ReferenceQueue 를 사용해야만 한다. 그래서 딱 1개 있는 PhantomReference 클래스의 생성자는 ReferenceQueue를 인자로 받는다.

ReferenceQueue<Object> rq = new ReferenceQueue<Object>();
PhantomReference<Object> pr = new PhantomReference<Object>(referent, rq);

phantomly reachable 는 파이널 라이즈와 메모리 회수 사이에 관여한다. strongly, softly, weakly 에 해당하지 않고 PhantomReference 로만 참조되는 객체는 파이널라이즈 된 이후에 phantomly reachable 로 간주된다. GC가 객체를 처리하는 순서는 항상 다음과 같다.

  1. soft references
  2. weak references
  3. 파이널라이즈 (가비지 컬렉션이 회수에하기 전에 객체가 리소스를 해제하고 다른 정리 작업을 수행할 수 있게 하는 것)
  4. phantom references
  5. 메모리 회수

 

즉, GC 여부를 판별할 때 어떤 객체의 reachability 를 strongly, softly, weakly 순서로 먼저 판별하고 모두 아니면 phantomly reachable 여부를 판별하기 전에 파이널라이즈 를 진행한다. 그리고 대상 객체를 참조하는 PhantomReference 가 있다면 phantomly reachable 로 간주해서 메모리 회수는 지연시키고 파이널 라이즈 이후 해야할 작업을 애플리케이션이 수행할 수 있도록 한다.

 

앞서 설명한 것처럼 PhatomReference는 항상 ReferenceQueue를 필요로 한다. 그리고 PhantomReference의 get() 메서드는 SoftReference, WeakReference와 달리 항상 null을 반환한다. 따라서 한 번 phantomly reachable로 판명된 객체는 더 이상 사용될 수 없게 된다. 그리고 phantomly reachable로 판명된 객체에 대한 참조를 GC가 자동으로 null로 설정하지 않으므로, 후처리 작업 후에 사용자 코드에서 명시적으로 clear() 메서드를 실행하여 null로 설정해야 메모리 회수가 진행된다.

 

이와 같이, PhantomReference를 사용하면 어떤 객체가 파이널라이즈된 이후에 할당된 메모리가 회수되는 시점에 사용자 코드가 관여할 수 있게 된다. 파이널라이즈 이후에 처리해야 하는 리소스 정리 등의 작업이 있다면 유용하게 사용할 수 있다. 그러나 개인적으로는 PhantomReference를 사용하는 코드를 거의 본 적이 없으며, 그 효용성에 대해서는 의문이 있다.

 

 

추가적인 설명


strongly reachable 객체는 unreachable 객체가 되면 GC에 의해서 메모리 상에서 회수된다.

 

발행/구독 패턴을 따르는 옵저버 디자인 패턴을 사용한 애플리케이션이나 웹소켓 연결에서 내부적으로 세션을 가지고 있는 경우, 또는 서버가 세션을 저장하는 방식의 애플리케이션의 경우 메소드 내부의 지역 변수 등등에서 해당 객체를 참조하고 있기 때문에 strongly reachable 객체이고 관련한 인스턴스가 살아있는 동안 메모리 상에서 사라지지 않는다.

 

그래서 Out Of Memory 가 발생할 수 있는데 이를 방지하고자 reference object 사용을 고려해볼 수 있다. 아래 테스트 코드를 보자

class BigData {
    private int[] array = new int[2500];
}

public class ReferenceTest {
    private List<WeakReference<BigData>> weakReferences = new LinkedList<>();
    private List<SoftReference<BigData>> softReferences = new LinkedList<>();
    private List<BigData> bigDatas = new LinkedList<>();

    public void weakReferenceTest() {
        try {
            while (true) {
                weakReferences.add(new WeakReference<BigData>(new BigData()));
            }
        } catch (OutOfMemoryError e) {
            System.out.println("out of memory");
        }
    }

    public void softReferenceTest() {
        try {
            while (true) {
                softReferences.add(new SoftReference<BigData>(new BigData()));
            }
        } catch (OutOfMemoryError e) {
            System.out.println("out of memory");
        }
    }

    public void strongReferenceTest() {
        try {
            while (true) {
                bigDatas.add(new BigData());
            }
        } catch (OutOfMemoryError e) {
            System.out.println("out of memory");
        }
    }

    public static void main(String[] args) {
        System.out.println("실행");

//        ReferenceQueue<Test> rq = new ReferenceQueue<Test>();
//        PhantomReference<Test> pr = new PhantomReference<Test>(new Test("test"), rq);


        ReferenceTest test = new ReferenceTest();
        test.weakReferenceTest();
//        test.softReferenceTest();
//        test.strongReferenceTest();

        System.out.println("종료");
    }
}

코드 출처 : https://ktko.tistory.com/entry/자바-강한참조Strong-Reference와-약한참조Weak-Reference

 

weakReferenceTest

weakReferenceTest 메소드는 동작하면 weakReferences 에 데이터를 쌓는다. 메모리 부족 타이밍에 GC 가 동작하고 WeakReference 에 의한 참조 값이 null 이 되어 unreachable 상태가 되어 메모리 상에서 회수 된다. 즉, OutOfMemoryError 가 발생하지 않는다.

 

softReferenceTest

softReferenceTest 메소드가 동작하면 softReferences 에 데이터를 쌓는다. 힙 메모리 영역이 점점 작아지면 위에서 언급한 공식에 의해 GC 대상이 되고 SoftReference 에 의한 참조 값이 null 이 되어 unreachable 상태가 된다. 메모리에서 회수되어 OutOfMemoryError 가 발생하지 않는다.

 

strongReferenceTest

strongReferenceTest 메소드가 동작하면 위와 같은 방식으로 bigDatas 에 데이터를 쌓는다. 이 ReferenceTest 의 메소드의 지역변수에서 참조하고 있기 때문에 GC 대상이 되지 않으며 결국 OutOfMemoryError 가 발생한다.

 

 

 

출처

https://d2.naver.com/helloworld/329631