(Effective Java 2/E) 106. Item 6 - Eliminate obsolete object references

Eliminate obsolete object references

  • 2판 제목 : 유효기간이 지난 객체 참조를 폐기하라
  • 3판 제목 : 다 쓴 객체 참조를 해제하라.

자바는 C/C++과는 다르게 가비지 컬렉터가 쓰지않는 메모리를 자동으로 회수해준다.

하지만 가비지 컬렉터는 만능이 아니다.

1. 메모리 누수

아래 스택을 구현한 예제를 보자.

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
29
30
31
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_CAPACITY];
}

public void push(Object e) {
ensureCapacity();
elements[size++] = 0;
}

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);
}
}
}

이 예제에서 잘못된 부분을 찾아보자.

ensureCapacity 함수가 호출되는 경우, 스택의 사이즈를 조절하고 있다.

이떄 스택의 사이즈를 늘어나기만하고, 줄어들진 않는다.

이는 단순히 메모리를 점유하는 것이 아니라 참조 상태를 유지하므로 가바지 컬렉터가 회수하지않게 된다.

좀 더 정확히는 스택이 미사용 객체에 대한 만기 참조(obsolete reference) 를 제거하지 않기 때문이다.

참고 만기 참조란 다시 이용되지 않을 참조를 뜻한다.

이 예제에서는 현재 요소들이 들어가있는 영역을 제외한 나머지 영역에 부여된 참조가 만기 참조이다.

이처럼 자동으로 메모리를 회수하는 가비지 컬렉터가 지원되는 언어에서 발생하는 의도치않은 객체의 보유를 메모리 누수(memory leak) 이라고 한다.

만기 참조를 해제하기 위해선 안 쓰는 객체의 참조는 무조건 null로 초기화하여 참조를 끊어버리면 된다.

위의 예제를 수정한다면 아래와 같다.

1
2
3
4
5
6
7
8
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}

하지만 모든 객체의 참조를 검증하고, 즉시 null로 초기화하는 것은 가비지 컬렉터를 가진 자바의 장점을 희석시키며 코드만 난잡해지기 십상이다.

따라서 미사용 객체의 참조를 끊어내는 것은 일종의 법칙이라기보단 예외적인 조치로 생각해야한다.

가장 좋은 방법은 참조가 보관된 변수가 유효 범위(scope)를 벗어나도록 하는 것이다.

이는 특정 변수를 정의할 때 그 유효 범위를 최대한 좁게 설정함으로서 자연스럽게 처리한다.

자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지않도록 주의해야 한다.

2. 캐시에서의 메모리 누수

캐시(cache)도 메모리 누수가 흔하게 발생하는 곳이다.

객체의 참조를 캐시 안에 넣어놓고 잊어버리는 경우가 많기때문이다.

이를 해결하기 위한 방법은 여러가지가 있다.

2.1. WeakHashMap

캐시를 구현할 때 WeakHashMap으로 구현하는 것도 하나의 방법이다.

캐시 외부에서 key를 참조하고 있는 경우에만 value를 보관하는 방식이다.

WeakHashMap 방식은 캐시 안에 보관되는 항목의 수명이 Key에 대한 외부 참조의 수명에 따라 결정되는 상황에서만 유효하다.

2.2. Background Thread

일반적으로 캐시에 보관되는 항목의 수명은 캐시에 보관된 기간에 따라 유동적으로 결정된다.

이런 방식의 캐시는 사용하지 않는 항목들을 캐시에서 제거해야하며, 이는 Background Thread인 Timer, ScheduledThreadPoolExecutor 등을 사용해서 처리할 수 있다.

반대로 항목을 추가할 때도 처리할 수 있다.

특히 LinkedHashMap 클래스를 사용하면 removeEldestEntry 함수를 통해 캐시를 소거하기 쉽다.

아래는 LinkedHashMap 클래스의 afterNodeInsertion 함수 구현체 코드이다.

1
2
3
4
5
6
7
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

3. Listener, Callback에서의 메모리 누수

만약 이미 사용이 완료된 Listener나 Callback를 명시적으로 제거하지않을 경우, 메모리를 점유하는 상태로 남아있게 된다.

상술한 WeakHashMap을 이용해 Key에 저장하는 방식처럼 약한 참조(weak reference) 로 저장하면 가비지 컬렉터가 즉시 처리할 수 있게 된다.