Stack으로 본 객체 참조 해제

아래의 Stack 클래스 코드를 확인해보고 문제가 될 만한 부분을 찾아보자.

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

특별히 문제가 보이진 않지만, 위의 코드는 꼭꼭 숨어 있는 메모리 누수 문제가 있다.
어디서 메모리 누수가 일어날까?

 

바로 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않기 때문이다.
프로그램에서 해당 객체들을 더 이상 사용하지 않더라도, 스택이 그 객체에 대한 참조를 가지고 있어서 가비지 컬렉터는 회수해가지 못한다.
객체 참조를 어디든 하나라도 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다.

 

따라서 이를 해결하기 위해서는 해당 참조를 다 썼을 때 null 처리를 해줘야 한다.

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

샘플 코드

객체 참조 해제 시 주의해야할 점

위와 같은 문제를 보고 걱정이 되어 모든 객체를 다 쓰자마자 일일이 null 처리할 필요는 없다. 또한 바람직하지도 않다.
객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

 

그렇다면 언제 null 처리를 해야 할까?
일반적으로 Stack 클래스와 같이 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.

캐시와 메모리 누수

캐시 역시 메모리 누수를 일으키는 주범이다.
객체 참조를 캐시에 넣고 나서, 다 쓴 뒤에 한참을 그냥 놔두는 일을 자주 접할 수 있다.
캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들자.

캐시를 만들 때 유효 기간을 정확히 정의하기는 어렵다. 이럴 때는

  1. ScheduledThreadPoolExecutor와 같은 백그라운드 스레드를 활용하거나
  2. 캐시에 새 엔트리를 추가할 때 부수 작업을 하는 방법이 있다(LinkedHashMapremoveEldestEntry).

리스너(listenr)와 콜백(callback)과 메모리 누수

클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백은 계속 쌓이게 된다.
이럴 때는 약한 참조(weak reference)를 사용한다. ex. WeakHashMap

메모리 누수 팁

메모리 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다.
그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.

반응형
복사했습니다!