Java ‐ 다 쓴 객체 참조를 해제하라[Effective Java Item 7] - thought-corner/Backend-PlayGround GitHub Wiki
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
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();
}
// [문제 발생 지점] 꺼내진 객체는 여전히 elements 배열이 참조하고 있음
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
pop()메서드를 호출하면 실제 값은 삭제되지 않고 인덱스만 한칸씩 이동하는 것으로 메모리 누수가 발생하게 된다.
// Good
public Object pop() {
if (size == 0) {
// [수정] 예외는 생성만 하는 것이 아니라 던져야(throw) 합니다.
throw new EmptyStackException();
}
Object result = elements[--size];
// [정석] 다 쓴 객체의 참조를 null로 해제하여 GC가 수거할 수 있게 함
elements[size] = null;
return result;
}
- 다 쓴 참조를
null처리하면 다른 이점도 따라온다. - 만약
null처리한 참조를 실수로 사용하게 되면 프로그램은 즉시 NPE를 던지며 종료된다. - 지역 변수로 선언된 컬렉션 타입은 메서드가 끝나면(=스코프에서 벗어나면) 더 이상 참조가 없기에 GC의 대상이 된다. 따라서 일반적으로 메서드가 종료되면 메모리가 해제된다.
- 반대로 그 컬렉션 타입이 static으로 선언되어 참조가 유지되면 GC가 회수되지 못해 메모리가 유지되어 잠재적 메모리 누수가 발생하게 된다.
❗주의사항
- 모든 객체를 다 쓰자마자 일일이
null처리하는 데 혈안이 되라는 말이 아니다.- 그럴 필요도 없고 바람직하지도 않기 때문이다. 오히려 프로그램을 필요 이상으로 지저분하게 만들 뿐이다.
- 객체 참조를
null처리하는 일은 예외적인 경우여야 한다.1. 자기 메모리를 직접 관리하는 클래스
- 자기 메모리를 직접 관리하는 클래스라면 개발자가 항시 메모리 누수에 주의해야만 한다.
- 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다
null처리해줘야 한다.2. 캐시(Cache)를 구현
- 객체 참조를 캐시에 넣고 나서 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 경우가 많다.
- 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 백그라운드 쓰레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법들이 있다.
3. 리스너 및 콜백(Listener & Callback)
- 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백은 계속 쌓여갈 것이다.
- 이를 방지하기 위해 등록 해제 시점에 참조를 끊어주어야 한다.