// 자바 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);
}
}
}
보통의 스택 코드이다.
위의 코드에서 스택이 커졌다 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
왜냐하면 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있기 때문이다.
그렇기에 메모리 누수가 일어날 수 있다고 한다.
그렇다면 메모리 누수를 막을 방법은 무엇일까?
해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다.
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
다 쓴 참조를 null 처리하면 다른 이점도 따라오는데 실수로 null 처리한 참조를 사용할 때 프로그램이 NullPointerException
을 던지며 종료된다.
하지만 이러한 처리를 일일이 모든 객체에 할 필요는 없고 이를 적용할 경우는 예외적인 경우여야 한다.
다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
그렇다면 null 처리는 언제 해야 하고 위의 Stack 클래스는 왜 메모리 누수에 취약한 걸까?
스택이 자기 메모리를 직접 관리하기 때문이다.
위 스택은 elements 배열로 저장소 풀을 만들어 원소를 관리한다.
배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다.
하지만 가비지 컬렉터는 이 사실을 알 수가 없다.
그렇기에 프로그래머가 비활성 영역이 되는 순간 null 처리를 해서 해당 객체를 더는 쓰지 않을 것임을 알려야 한다.
캐시 역시 메모리 누수를 일으키는 주범이다.
객체 참조를 캐시에 넣고 나서, 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다.
운 좋게 캐시 외부에서 키를 참조하는 동안만(값이 아닌!) 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap
을 사용해 캐시를 만들면 다 쓴 엔트리는 즉시 자동으로 제거될 것이다.
캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다.
이러한 경우에는 Scheduled ThreadPoolExecutor
같은 백그라운 스레드를 활용하거나 캐시를 LinkedHashMap
을 사용하여 구현해 removeEldestEntry
메서드를 사용해 새 엔트리를 추가할 때 부수 작업으로 스레드를 청소하는 방법이 있다.
클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치하지 않는 한 콜백은 계속 쌓여간다.
이럴 때 콜백을 약한 참조로 저장하면 가비지 컬랙터가 즉시 수거한다.
예를 들어 WeakHashMap
에 키로 저장하면 된다.
오.. 하루에 포스팅 두 개..