다 쓴 객체 참조를 해제하라
자바는 가비지 컬렉션으로 다 쓴 객체를 알아서 회수해준다. 따라서 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 생각하지만, 절대 사실이 아니다.
*가비지 컬렉션: JVM의 힙 영역에서 사용 중인 객체와 그렇지 않은 객체를 식별하고 사용하지 않는 객체를 삭제하는 프로세스
*가비지 컬렉선의 대상이 되려면 해당 객체를 가리키는 레퍼런스가 전부 사라져야한다. 즉, 어떤 객체를 WeakReference 객체만 참조하고 있을 경우 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);
}
}
위의 코드는 메모리 누수라는 큰 문제가 존재한다. 이 코드를 사용하는 프로그램을 오래 실행하면, 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하된다.
여기서 스택이 커졌다가 줄어들었을 때 (pop) 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않아 메모리 누수가 생긴다. 따라서 위의 문제는 아래 코드와 같이 null
로 참조해제를 해주면 된다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
만약 null 처리한 참조를 실수로 접근하게 된다면, NullPointerException
을 발생시키며 종료할 수 있다.
이전의 코드가 메모리 누수에 취약한 이유는 스택이 자기 메모리를 직접 관리하기 때문이다. elements
배열로 저장소 풀을 만들어 원소들을 관리하며, 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다. 따라서 가비지 컬렉터는 이 사실을 알 수 없고 프로그래머만 아는 사실이기에 메모리 누수에 취약하였다. 즉, 프로그래머는 비활성 영역이 되는 순간 null
처리해서 해당 객체를 사용하지 않음을 알려야 한다.
객체 참조를 캐시에 넣고 나서 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다. 해법 중 하나는 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap
을 사용하자. 다 쓴 엔트리는 자동으로 제거가 된다.
캐시를 만들 때 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 주로 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 청소해줘야 한다.
클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백은 쌓인다. 이럴 때 콜백을 Weak reference로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어 WeakHashMap
에 키로 저장하면 된다.