자바에서는 GC를 통해 메모리 관리에 유연해진다.
아래 코드는 별 문제가 없어 보인다.
public class Stack {
private Object[] elements;
// Something...
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
}
하지만 스택을 사용하는 프로그램을 오래 실행하다보면 점차 GC 활동과 메모리 사용량이 늘어나 성능이 저하될 것이다. 상대적으로 드문 경우긴 하지만 디스크 페이징이나 OOM을 일으켜 프로그램이 예기치 않게 종료되기도 한다.
위 pop()
메서드는 스택에서 꺼내진 객체들을 GC가 회수하지 않는다. 객체의 참조를 배열이 가지고 있기 때문이다.
GC 언어에서는 메모리 누수를 찾기가 까다롭다. 객체 참조 하나를 살려두면 GC는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수하지 못한다.
해법은 간단하다. 참조를 다 썼을 때 null 처리하면 된다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return elements[--size];
}
그렇다고 해서 모든 객체를 다 쓰자마자 일일이 null 처리하는 것은 코드를 지저분하게 한다. 객체 참조를 null 처리하는 것은 예외적인 경우여야 한다.
객체 참조를 캐시에 넣고, 이를 잊은 채 그 객체를 다 쓴 뒤로 놔두는 일을 접할 수 있다.
외부에서 키를 참조하는 동안만 엔트리가 살아 있는 상황이라면 WeakHashMap을 사용해 캐시를 만들 수 있다.
캐시를 만들 때 엔트리의 TTL을 정하는 것이 가장 어렵다. 따라서 시간이 지날수록 엔트리의 가치를 떨어트리는 방식을 사용한다. 이런 방식에서는 엔트리를 정리해야 한다. 백그라운드 스레드를 이용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다.
LinkedHashMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 정리한다.
클라이언트가 콜백을 등록만 하고 명확히 해제하지 않는다면, 콜백이 계속 쌓일 수 있다. 이럴 때 콜백을 약한 참조로 저장하면 GC가 즉시 수거해간다.
메모리 누수는 겉으로 드러나지 않는다. 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다. 문제의 예방법을 익혀두자.