메모리 누수
다음 코드는 memory leak을 유발한다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public 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);
}
}
이 코드는 스택에 push를 수행한 후 pop을 했을 때 꺼내진 객체를 GC가 수거하지 않는다. 프로그램이 그 객체를 더 이상 사용하지 않더라도 말이다. 이 스택이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다. 해법은 간단하다. 해당 참조를 다 썼을 때 null 처리하면 된다.
1 | public Object pop() { |
다 쓴 참조를 null 처리하면 이점도 따라온다. 만약 null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료한다. 하지만 여기에 혈안이 될 필요는 없다. 프로그램을 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 스코프 밖으로 밀어내는 것이다. 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이루어진다.
그렇다면 null 처리는 언제 해야 할까? Stack 클래스는 왜 메모리 누수에 취약한 걸까? 바로 스택이 자기 메모리를 직접 관리하기 때문이다. 이 스택은 elements 배열로 저장소 풀을 만들어 원소들을 관리한다. 배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다. 문제는 GC는 이 사실을 알 길이 없다는데 있다. 그러므로 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.
캐시 메모리
캐시 역시 메모리 누수를 일으키는 주범이다. 객체 참조를 캐시에 넣고 나서, 이 사실을 까맣게 잊은 채 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다. 해법은 여러가지다. 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 사용하면 된다. 다 쓴 엔트리는 그 즉시 자동으로 제거된다. 캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 이따끔 청소해줘야 한다.
리스너 혹은 콜백
리스너혹은 콜백 역시 메모리 누수의 주범이다. 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 뭔가 조치를 해주지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조로 저장하면 GC가 즉시 수거해간다.