Effective Java 3/E
아이템7: 다 쓴 객체 참조를 해제하라
1. 메모리 누수 해결은 다 쓴 참조를 null 처리하자
GC가 다 쓴 객체를 알아서 회수하기 때문에 메모리 신경을 쓰지 않아도 된다고 오해할 수 있다.
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 object) {
ensureCapacity();
elements[size++] = object;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때 마다 대략 2 배씩 늘린다.
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 코드를 보기에 문제가 없어보이지만 메모리 누수의 문제를 가지고있다.
스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체( pop() 으로 꺼내진 객체 )는 더 이상 사용하지 않는다고 하더라도 GC 가 회수 하지 않는다.
결국 이 스택이 객체들의 다 쓴 참조(obsolete reference) 를 계속 갖고 있게 되고, 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하되게 된다.
이에 대한 해법은 아주 간단하다. pop() 실행 시 꺼내진 객체는 null 처리 해주면 된다.
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
2. 예외적인 null 처리
Stack은 자기 메모리를 직접 관리하고, 객체 자체가 아니라 객체참조를 담는 elements 배열로 저장소 풀을 만들어 원소를 관리한다.
배열의 활성 영역에 속한 원소들이 사용되고, 비활성 영역은 쓰이지 않는다. GC 가 보기엔 이 비활성 영역의 객체들도 같은 객체이다. 따라서 다 쓴 참조를 null 처리 해주어 이제 더이상 쓰지 않는 다는 것을 GC에게 알려 메모리 누수를 방지 할 수 있다.
3. 메모리 누수를 주의해야 하는 경우
1. 자기 메모리를 직접 관리하는 클래스
- 위 스택의 경우와 같이 원소를 다 사용한 즉시 원소가 참조한 객체들은 null처리 해주어야 한다.
2. 캐시
- 객체 참조를 캐시에 넣고, 사용이 끝난 이후에도 참조를 해제하지 않은 경우
캐시 메모리 누수 해결
1. WeakHashMap 을 이용하여 캐시를 만들자!
-> 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다!
2. 시간이 지날 수록 엔트리의 가치를 떨어뜨리고 ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 앤트리를 추가할 때 부수 작업으로 수행하여 메모리 청소
3. LinkedHashMap 은 removeEldestEntry 메소드를 사용하여 앤트리 추가 시 부수작업으로 수행
4. 복잡한 캐시를 생성할 땐 java.lang.ref 패키지를 직접 활용해야함.
3. 리스너(Listener) 혹은 콜백(Callback)
- 클라이언트가 콜백을 등록만하고 해지하지 않는 경우 콜백은 계속 쌓인다.
-> WeakHashMap 에 키로 저장한다. ( 콜백을 약한 참조로 저장하면 GC가 수거해간다.)