본문 바로가기

Dev Book Review/Effective Java

[Effective Java] item7. 다 쓴 객체 참조를 해제하라

1. GC의 메모리 누수 : null로 해제

GC가 다쓴객체를 알아서 회수해간다고, 메모리 관리에 아예 신경을 안쓰면 안된다.

// 코드 7-1 메모리 누수가 일어나는 위치는 어디인가? (36쪽)
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

//    // 코드 7-2 제대로 구현한 pop 메서드 (37쪽)
//    public Object pop() {
//        if (size == 0)
//            throw new EmptyStackException();
//        Object result = elements[--size];
//        elements[size] = null; // 다 쓴 참조 해제
//        return result;
//    }

    public static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : args)
            stack.push(arg);

        while (true)
            System.err.println(stack.pop());
    }
}

메모리 누수 문제 = 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하된다.

스택에서 pop()이 된 객체를 GC가 회수하지 않는다. 프로그램에서 그 객체를 더이상 사용하지 않아도! (다 쓴 참조를 여전히 갖고있어서)

GC의 메모리 누수 : 객체 참조 하나를 살두면, 이 객체뿐 아니라, 이 객체가 참조하는 모든 객체를 회수하지 못한다.
solution : 해당 참조를 다 썼을 때 null 처리한다.

 

2. Null 처리는 예외적이다.

객체 참조를 Null 처리하는 일은 예외적인 경우여야한다.참조를 담은 변수를 유효 범위(scope) 밖으로 밀어낸다.

  • 스택은 자기 메모리를 직접 관리한다.
  • 배열의 활성영역에 속한 원소만 사용되고 비활성 영역은 쓰이지 않는다. (GC가 알 방법이 없다.)
  • 비활성 영역이 되는 순간 Null 처리로 GC에게 알릴 때 사용한다.

 

3. 메모리 누수의 주범

a. 자기 메모리를 직접 관리하는 클래스

프로그래머가 항시 메모리 누수에 주의한다. (ex stack)

 

b. 캐시

객체 참조를 캐시에 넣고, 객체를 다 쓴 후에도 놔누는 경우

해결책

  • WeakHashMap을 사용해 캐시를 만든다. -> 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시
  • 시간이 지날수록 엔트리 가치를 떨어뜨리는 방식 ->ScheduledThreadPoolExecutor (쓰지 않는 엔트리 청소)

 

c. 리스너(listener), 콜백(callback)

클라이언트가 콜백을 등록만하고 해지하지 않는경우, 계속 쌓인다.

해결책

  • 콜백을 약한 참조(weak reference)로 저장한다. -> WeakHashMaP에 키로 저장한다.

 

4. 참고자료

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

java.lang.ref 패키지의 객체 참조 종류 4가지

  • strong reference
  • soft reference
  • weak reference
  • phantom reference

LRU(least recently used) 캐시 같은 애플리케이션 쉽게 작성가능

 

GC의 reachability

reachable : 어떤 객체에 유효한 참조가 있다. (root set : 유효한 최초의 참조)

unreachable : 어떤 객체에 유효한 참조가 없다. (GC 대상)

 

soft, weak, phantom reference

- reference Object

SoftReference, WeakReference, PhantomReference 3가지 클래스에서 생성된 객체

 

- WeakReference, Weak Reachable

WeakReference<Sample> wr = new WeakReference<Sample>(new Sample());
Sample ex = wr.get(); // 참조되었다.
ex = null; // weakly reachable 객체

GC 동작마다 회수된다. LRU 캐시 구성시 주로 WeakReference를 사용한다.

WeakReferencerence 객체내의 weakly reachable 객체에 대한 참조가 null로 설정되면, GC에 의해 메모리 회수

 

- SoftReference, Softly Reachable

Strong reachable이 아니면서 SoftReference 객체로만 참도된 객체자주 사용될수록 더 오래 살아남는다.

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

SoftReferencerence 객체내의 softly reachable 객체에 대한 참조가 null로 설정되면, GC에 의해 메모리 회수

 

- PhantomReference, Phantomly Reachable

GC 대상 객체를 처리하는 작업 (= 객체의 파이널 라이즈 작업) 이후에 GC 알고리즘 따라 메모리 회수한다.

Softly Weakly : GC 대상 여부를 판별Phantomly : 파이널라이즈 이후 작업을 수행하게 한다. Phatom reference를 reference queue에 넣음

파이널라이즈 이후 처리해야하는 리소스 정리 등 작업에서 유용하게 사용할 수 있다. (거의 사용사지 않는다.)

 

Reference Queue

WeakHashMap 클래스 : Reference Queue + WeakReference 를 사용해 구현되어있다.

Phantom Reference는 참조된 객체를 phantomly reachable로 만들고 eunque한다.

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