자바는 C,C++과 달리 가비지 컬렉터가 다 쓴 객체를 알아서 회수해 메모리를 자동으로 관리해준다. 마치 프로그래머는 이를 신경쓸 필요가 없어보이지만 전혀 그렇지 않다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFULAT_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 enusreCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 코드는 간단한 스택을 구현한 것인데 스택이 커졌다가 줄어들 때 스택에서 꺼낸 객체들을 가비지 컬렉터가 회수하지 않기 때문에 메모리 누수
가 일어날 가능성이 있다. 스택이 그 객체들을 더 이상 참조 하지 않는데도 불구하고 갖고 있기 때문이다.
elements 배열의 '활성 영역' 밖의 참조들이 모두 여기에 해당한다. 활성영역은 size보다 작은 원소들로 구성된다.
객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못하기 때문에 메모리 누수를 찾기가 아주 까다롭다.
해법은 해당 참조를 다 썼을 때 null 처리를 해주면 된다. 스택에서는 원소의 참조가 더 이상 필요 없어지는 때가 스택에서 꺼내질 때다.
public Object pop() {
if(size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
만약 다 쓴 참조를 null 처리하면 혹여 실수로 다시 그 객체를 사용하려 할 때 NullPointerException을 반환하게 될 것 이다. 만약 null 처리를 하지 않고 계속 썼다면 잘못된 일을 수행할 수도 있을 것이다.
하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 모든 객체에 일일히 그렇게 해줄 필요가 없다는 말이다 .다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
그렇다면 이 null 처리는 언제 해야할까? Stack에 경우는 메모리를 직접 관리하기 때문에 (객체 참조(객체자체x)를 담는 elements 배열로 저장소 풀을 만든다. ) 배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다. 가비지 컬렉터는 이를 인식하지 못하고 또 가비지 컬렉터가 보기에는 비활성 영역에서 참조하는 객체도 같은 유효 객체로 보인다.
일반적으로 자기 메모리를 직접 관리하는 클래스면 프로그래머는 메모리 누수에 신경을 쓰는 것이 좋다.
캐시 역시 메모리 누수를 일으키는 주범이다. 객체 참조를 캐시에 넣고 이를 잊은 채 그 객체를 다 쓴 뒤로 한참 놔두는 일이 빈번하게 일어난다. 만약 운좋게 캐시 외부에서 키(Key)를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들어라. 다 쓴 엔트리는 자동으로 제거된다.
캐시를 만들 때 보통은 캐시 엔트리의 유효기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 주기적으로 청소해줘야한다.(ScheduledThreadPoolExecutor 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수적 작업으로 수행하는 방법이 있다. LinkedHashMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다. 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야할 것이다.
리스너(Listener) 혹은 콜백(Callback)도 메모리 누수와 아주 관련이 깊다. 클라이언트가 콜백을 등록만하고 명확히 해지하지 않는다면 계속 쌓일 가능성이 높다. 이럴 때 콜백을 약한 참조
로 저장하면 가비지 컬렉터가 즉시 수거해간다.(WeakHashMap에 키로 저장하면 된다.)