다 쓴 객체 참조를 해제하라
C, C++ 처럼 메모리를 직접 관리하는 언어와 다르게 자바는 GC에 의해 메모리를 관리한다. 하지만 메모리 관리에 신경 써야한다.
스택을 간단히 구현한 예제 코드를 살펴보자.
public class Stack {
private Object[] elements;
private int size;
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()
에서 메모리 누수 문제가 발생한다.- 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들은 사용하지 않더라도 GC가 회수하지 않는다. 즉 다 쓴 참조 (obsolete reference)를 갖고 있는다.
elements
배열에서 size(활성 영역) 크기를 넘어가는 객체들은 모두 다 쓴 참조 상태가 되며, GC가 회수하지 않는다.
- GC는 메모리 누수를 찾기가 까다롭다. 그 이유는 객체 참조를 하나 살려두면, 그 객체가 참조하는 모든 객체를 회수해가지 못한다.
다 쓴 참조 객체를 회수하는 방법
다 쓴 참조 객체를 회수하는 방법으로는 null
처리, 캐시 엔트리 처리, 콜백 처리 등이 있다.
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 객체 회수
return result;
}
- 다음과 같이 참조를 사용 후
null
처리를 통해 참조를 해제하면 해결 된다. 또한 해당 처리로 인해 실수로 null인 인덱스에 참조하게 되면NPE
로 오류를 인지하기 수월해진다.
하지만 객체 참조를 null
처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
그렇다면, null
처리는 언제할까? 바로 메모리를 직접 관리하는 경우이다. 위의 스택 예제는 elements
를 배열로 저장소 풀을 만들어 원소를 관리한다. 문제는 이 사실을 GC가 알 수 없다. GC의 입장에선 현재 elements
의 배열 원소 중 활성 영역과 비활성 영역 모두 똑같은 유효 객체로 판단하기 때문이다. 따라서 메모리를 직접 관리하는 경우 비활성 영역이 되는 순간 null
처리를 해서 해당 객체를 사용하지 않음을 GC에게 알려야 한다.
캐시 역시 메모리 누수의 주범이다. 캐시를 만들 때 엔트리(키 값)의 유효 기간을 정확히 정의하기 어렵다. 따라서 흔히 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다. 이런 방식에는 백그라운드 스레드를 활용하는 ScheduledThreadPoolExecutor
나 새 엔트리를 추가할 때 부수 작업으로 수행하는 LinkedHashMap
의 removeEldesEntry()
를 사용한다. 더 복잡한 캐시를 만드록 싶다면 java.lang.ref
패키지를 직접 활용해야 한다.
메모리 누수의 세 번째 주범은 바로 linstener
혹은 callback
이다. 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 계속 쌓일 것이다. 이럴 때 콜백을 약한 참조 (weak reference
)로 저장하면 GC가 즉시 수거해간다. 예를 들어 WeakHashMap
에 키로 저장하면 된다.
메모리 누수는 겉으로 잘 드러나지 않아서 시스템에 수년간 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야 발견되기도 한다. 따라서 예방법을 익혀두는 것은 중요하다.