자바에서는 가비지 컬렉터가 다쓴 객체를 알아서 회수해간다 하지만 그렇다고해서 메모리 관리에 신경쓰지 않으면 안된다. 메모리 누수가 발생하는 프로그램을 오래 실행하다보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하되거나 메모리초과(OufOfMemory
) 오류가 발생할 수 있다.
[메모리 누수가 일어나는 스택 구현]
public class StackMemoryLeak {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity(e);
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return elements[size--];
}
private void ensureCapacity(final Object e) {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
그 이유는 스택이 그 객체들의 다 쓴 참조(obsolete reference) 를 여전히 가지고 있기 때문이다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 따라서 잠재적으로 성능에 악영향을 줄 가능성이 있다.
앞의 코드에서는 elements
배열의 '활성 영역' 밖의 참조들이 모두 여기에 해당된다.
('활성 영역' 은 인덱스가 size
보다 작은 원소들로 구성된다.)
메모리 누수를 방지하는 방법은 간단하다. 해당 참조를 다 사용했을 때 null
처리(참조 해제)하면 된다.
[메모리 누수를 방지하는 스택 pop메서드]
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
다 쓴 참조를 null
처리하면 따라오는 부수적인 이점이 있다. 만약 null
처리한 참조를 사용하려하면 NullpointerException
이 발생하며 종료된다.
사실 모든 객체를 다 쓰자마자 null
처리하는 것은 별로 바람직하지 않다. 객체 참조를 null
처리하는 일은 예외적인 경우여야 한다.
다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
참조를 변수 유효범위 밖으로 밀어내는 일은 변수의 범위를 최소로 정의했다면 자연스럽게 이루어진다.
Stack
클래스가 메모리 누수에 취약한 이유는 자기 메모리를 직접 관리하기 때문이다.
일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야하고 원소를 다 사용하면 참조한 객체를 다
null
처리해줘야 한다.
위에서 예를 들었던 스택은 객체 자체가 아니라 객체 참조를 담는 elements
배열로 메모리를 관리한다. 문제는 배열의 비활성 영역은 쓰이지 않는다는 것이고 가비지 컬렉터는 이 사실을 알지 못한다.
그러므로 프로그래머는 비활성 영역이 되는 순간 null
처리를 통해 해당 객체가 더 이상 쓰이지 않는다는 사실을 가비지 컬렉터에게 알려주어야 한다.
객체 참조를 캐시에 넣어두고 이를 방치하는 일이 자주 발생한다. 이를 해결하는 방법에는 여러가지가 있다.
WeakHashMap
을 사용해 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시를 사용하는 방식.ScheduledThreadPoolExecutor
같은 백그라운드 스레드를 활용해 엔트리를 청소하는 방식.LinkedHashMap
의 removeEldesEntry
메서드를 활용해 엔트리를 청소하는 방식.java.lang.ref
패키지를 이용해 더 복잡한 캐시를 만드는 방법.클라이언트가 콜백을 등록만하고 명확히 해지하지 않는다면, 콜백은 계속 쌓이게 된다. 이럴 때 콜백을 약한 참조(weak reference
)로 저장하면 가비지 컬렉터 가 즉시 수거해 간다.
ex) WeakHashMap
에 키로 저장하면 된다.
일반적인
HashMap
의 경우Key
와Value
가 put되면 사용여부에 관계없이 해당 내용은 제거되지 않는다. 하지만weakHashMap
의 경우Key
에 해당하는 어떤 객체가null
이 되면 해당 객체를key
로 하는HashMap
의Element
도 더이상 사용하지 않는다고 판단되어 자동으로 제거되어 버린다.
[WeakHashMap]
public class WeakHashMapTest {
public static void main(String[] args) {
final WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>();
Integer key1 = 1000;
Integer key2 = 2000;
Integer key3 = 3000;
weakHashMap.put(key1, "test1");
weakHashMap.put(key2, "test2");
weakHashMap.put(key3, "test3");
weakHashMap.entrySet().forEach(System.out::println);
weakHashMap.put(1000, null);
System.gc(); // gc 실행 후 결과
weakHashMap.entrySet().forEach(System.out::println);
key1 = null;
weakHashMap.entrySet().forEach(System.out::println);
System.gc();
weakHashMap.entrySet().forEach(System.out::println);
}
}
// 실행결과
3000=test3
2000=test2
1000=test1
3000=test3
2000=test2
1000=null
3000=test3
2000=test2
1000=null
3000=test3
2000=test2
메모리 누수는 겉으로 잘 드러나지 않기 때문에 이런 종류의 문제는 예방법을 익혀두는것이 중요하다. 그렇지 않으면 디스크 페이징이나 OutOfMemoryError
를 일으켜 프로그램이 예기치 않게 종료될 수 있다.
이렇게 메모리누수를 방지하는 방법으로 다 쓴 참조 객체를 null
처리 해주는 방법이 있다. 하지만, 사실 모든 객체를 다 사용 후 null
처리하는것은 별로 바람직하지 않다.
다 사용한 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수의 범위를 최소한으로 지정하여 사용 후 유효범위 밖으로 자연스레 밀려나게 하는 것이다.
단, 자기 메모리를 직접 관리하는 클래스의 경우 참조한 객체를 사용 후 null
처리해주어야 한다.
예를들어, 위에서 본 스택의 경우 스택에서 꺼내진 객체들에 대한 다 쓴 참조를 여전히 스택이 가지고 있다. 꺼내진 객체들은 비활성영역에 해당하는 객체들인데 가비지 컬렉터는 이런 비활성영역에 대한 객체들을 회수하지 못한다. 따라서 이런 경우 null
처리를 통해서 객체가 사용하지 않는다는 사실을 가비지 컬렉터에게 알리고 메모리 누수를 방지하자.