April 07, 2022
자바에서는 가비지 컬렉터가 다 쓴 객체를 알아서 회수해간다. 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아니다.🙅🏻♂️
메모리 누수가 일어나는 위치가 어딜까?
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);
}
}
}
메모리 누수가 어디서 일어날까?
이 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있기 때문이다.
다 쓴 참조(obsolete reference)란 다시 쓰지 않을 참조를 뜻함
가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체…)를 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수도 있고 잠재적으로 성능에 악영향을 줄 수 있다.
-> 해법 : 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다.
// 제대로 구현한 pop 메서드
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
다 쓴 참조를 null 처리하면 다른 이점도 따라온다.
null 처리는 언제 해야 할까?
Stack 클래스는 왜 메모리 누수에 취약한 걸까? 스택이 자기 메모리를 직접 관리하기 때문이다.
자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다. 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 한다.
캐시 역시 메모리 누수를 일으키는 주범이다.
해법
클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조(weak referernce)로 저장하면 가비지 컬렉터가 즉시 수거해간다. (WeakHashMap에 키로 저장하면 된다.)