[EffectiveJava] item 7. 다 쓴 객체 참조를 해제하라.

item 7

다 쓴 객체 참조를 해제하라.


다음 코드를 살펴보자.

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

=> 위의 코드는 문제가 없어보이지만 문제가 확실히 있다.
바로 메모리 누수로, 위 코드에서 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 프로그램에서 그 객체들을 더 이상 사용하지 않더라도 말이다.
바로 이 스택이 그 객체들의 다 쓴 참조(obsolete reference) 를 여전히 가지고 있기 때문인데,
pop() 메소드를 보면 이 스택이 저장된 객체들을 빼내도 여전히 그 객체들을 참조함을 알 수 있다. 가령 스택의 현 size 가 2이고 elements[0] = ‘a’, elements[1] = ‘b’일때 pop 해서 ‘b’를 빼낸다 치면, 스택의 size 는 1이 되고 인덱스 1인 곳에는 ‘b’가 없을거라 생각할 수 있지만, 애초에 스택의 크기가 16이기 때문에 스택은 여전히 b를 참조하고 있으므로 가비지 컬렉터는 이 다 쓴 참조 를 회수할 수 없는 것이다.
위의 코드에서는 elements 배열의 ‘활성 영역’ 밖의 참조들이 모두 다 쓴 참조에 해당한다. 활성 영역은 인덱스가 size 보다 작은 원소들로 구성된다.


null 처리 (참조 해제), 제대로 구현한 pop 메서드

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

=> 해법은 간단하다. 해당 참조를 다 썼을 때 null(참조 해제)하면 된다. 예시의 스택 클래스에서는 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 꺼내질 때다.
따라서 위와 같이 pop()의 코드를 재수정하였다.

메모리를 직접 관리하는 클래스는 프로그래머가 메모리 누수를 조심해야 한다.(null 처리해야 한다.)

캐시

캐시를 사용할 때도 메모리 누수 문제를 조심해야 한다. 객체의 레퍼런스를 캐시에 넣어 놓고 캐시를 비우는 것을 잊기 쉽다. 여러 가지 해결책이 있지만, 캐시의 키에 대한 레퍼런스가 캐시 밖에서 필요 없어지면 해당 엔트리를 캐시에서 자동으로 비워주는 WeakHashMap을 쓸 수 있다.
또는 특정 시간이 지나면 캐시값이 의미가 없어지는 경우에 백그라운드 쓰레드를 사용하거나 (아마도 ScheduledThreadPoolExecutor), 새로운 엔트리를 추가할 때 부가적인 작업으로 기존 캐시를 비우는 일을 할 것이다. (LinkedHashMap 클래스는 removeEldestEntry라는 메서드를 제공한다.)

콜백

세번째로 흔하게 메모리 누수가 발생할 수 있는 지점으로 리스너와 콜백이 있다. 클라이언트 코드가 콜백을 등록할 수 있는 API를 만들고 콜백을 뺄 수 있는 방법을 제공하지 않는다면, 계속해서 콜백이 쌓이기 할 것이다. 이것 역시 WeahHashMap을 사용해서 해결 할 수 있다.
메모리 누수는 발견하기 쉽지 않기 때문에 수년간 시스템에 머물러 있을 수도 있다. 코드 인스택션이나 heap profiler 같은 디버깅 툴을 사용해서 찾아야 한다. 따라서 이런 문제를 예방하는 방법을 학습하여 미연에 방지하는 것이 좋다.