DEV ℧ Developer Diary

[EffectiveJava] item07 - 다 쓴 객체 참조를 해제하라.

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

자바는 C, 와 C++과는 다르게 JVM에서 GC(Garbage Collection)에 의해 더이상 사용하지 않는 개체들을 메모리단에서 정리해준다. 반면에 C와 C++은 GC같은 개념이 없이 메모리 해제를 하지 않으면 시스템을 재시작 하지 않는 이상 메모리에 계속 남아있게 된다.

하지만 JVM에서 작동하고 있는 GC도 만능이 아니다.

아래의 간단한 예시를 보자.

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);
    }
}

특별한 문제는 없어보이는 그저 Stack을 구현한 클래스이다. 만약 나도 책에서 문제점을 꼬집어주지 않는다면 절대 몰랐을 것 같은 코드이다.

해당 소스에 해당하는 문제는 메모리 누수가 있는데, 해당 스택 클래스를 사용 하는 프로그램은 지금 당장은 괜찮을지 몰라도, 점차 가비지 컬렉션의 활동과, 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 최악의 상황이라면, 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 못하게 종료가 될 수도 있다.

해당 코드에서 메모리 누수는 어디에서 일어날까? 해당 코드에서는 스택이 ensureCapacity()에 의해 커지거나 줄어들 때 스택에서 꺼내진 객체들은 GC가 회수 하지 않는다. 만약 프로그램에서 해당 객체를 사용하지 않는다고 해도 말이다.

왜냐? 위의 코드에서 작성된 Stack에서 해당 객체의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

다 쓴 참조란? 문자 그대로 앞으로 다시 쓰지 않을 참조를 뜻한다.

결국 위의 코드에 elements배열의 ‘활성 영역’ 밖의 객체들의 참조가 모두 여기에 해당한다.

자바의 가비지 컬렉션 언어에서 메모리 누수를 찾기는 정말 까다롭다. 객체 참조 하나를 살려두면 그 객체가 참조하는 모든 객체들을 회수해 가지 못하기 때문에, 잠재적으로 성능에 악영향을 줄 수 있다.

그래서 해법은 무엇일까? 바로 다쓴 참조에 대해서 null처리를 해주면 된다. 위의 코드에서 pop 메소드를 수정해서 메모리 누수를 줄여보자.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; /* 다 쓴 참조를 해제한다. */
    return elements[--size];
}

아래는 실제 java.util.Stack에서 쓰이는 null처리 방법이다.


public synchronized E pop() {
    E       obj;
    int     len = size();

    obj = peek();
    removeElementAt(len - 1);

    return obj;
}

public synchronized void removeElementAt(int index) {
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    } else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    modCount++;
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

pop 호출 이후 removeElementAt메서드를 동기화하여 사용하지 않는 elemant를 null처리 해주고 있다.

해당 다 쓴 참조를 null 처리한다면, 생기는 이점도 있다. 실수로 해당 객체를 사용 할경우 nullPointException으로 예외처리를 하게되는데, null 처리가 없이 잘못 사용하게 된다면 프로그램이 오작동을 일으킬 가능성이 있다.

그렇다고 모든 참조 객체를 null을 넣어서 해제할 필요는 없다. 프로그램을 필요이상으로 지저분하게 만들 수 있으므로 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

그렇다면 언제 참조를 해제해 주는것이 좋을까?

일반적으로는 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 하기 때문에 참조 해제를 추가해주면 좋다. 예를들면, 위의 코드에서 작성된 스택이나 캐시 등등이 해당된다.

캐시의 경우 사용된 후 이따금씩 참조된 메모리들을 청소해 줄 필요가 있다.