ITEM 7 "다 쓴 객체 참조를 해제하라"
이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.
열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.
자바에는 GC (Garbage Collector) 가 있기 때문에 메모리 관리에 대해 신경을 쓰지 않아도 될 것이라고 생각하기 쉽지만, 사실 그렇지 않다. GC 가 언제 메모리를 회수해 가는지 정책(?) 을 잘 알아야 메모리 누수 문제에 해방될 수 있다.
public class Stack {
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) {
this.ensureCapacity();
this.elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
return this.elements[--size]; // 여기가 문제!
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
private void ensureCapacity() {
if (this.elements.length == size) {
this.elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 스택을 구현한 코드는 메모리 누수 문제가 있다.
스택에 아이템을 계속 쌓았다가 다시 빼냈다고 가정해보자. 스택이 차지하는 메모리가 줄어들 것이라고 예상이 되지만, 줄어들지 않는다. pop 메서드에서 this.element 를 리턴하고 있는데 배열의 인덱스만 줄어들었을 뿐 배열의 크기는 조정이 된 것이 없기 때문에 계속해서 메모리만 할당이 되고 해제가 되지 않는 것이다.
그렇다고 해도 GC 가 주기적으로 검사하여 해제를 할 수 있을 것 같은데 무엇이 문제일까?
GC 는 scope 중심으로 scope 가 끝나는 지점에 더 이상 사용이 되지 않는 변수들을 해제한다. 하지만 위와 같이 직접 메모리를 관리하는 경우, 명시적으로 null 을 삽입해주어야 해제 대상이 된다. 위 스택 코드를 보면 element 배열에 있는 값을 그대로 두고 인덱스만 조정하기 때문에 값은 그대로 있게 되고 해제 대상이 되지 않는 것을 볼 수 있다. (객체들의 다 쓴 레퍼런스를 그대로 가지고 있다라고 표현한다.)
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object value = this.elements[--size];
this.elements[size] = null;
return value;
}
위와 같이 스택에서 꺼낼 때 그 자리를 null 로 설정해주면 GC 가 발생할 때 레퍼런스가 정리된다. 실수로 null 처리한 참조를 사용하여 NullPointerException 이 발생할 수 있지만, null 이 처리되지 않고 인식하지 못 한 상태에서 값이 사용되는 것보단 낫다. 프로그래밍 에러는 언제든지 빨리 포착하는 것이 유익하다.
하지만, 모든 필요 없는 객체를 null 로 만들 필요는 없다. 그 레퍼런스를 가리키는 변수를 스코프 안에서만 사용한다면 문제가 없다. ("변수를 가능한 가장 최소의 scope 안에서 처리하라" 라는 규칙만 지킨다면 자연스럽게 해결할 수 있다.)
보통은 위와 같은 규칙을 지키면 문제가 없고, 스택 코드처럼 메모리를 직접 관리하는 코드만 메모리 누수에 주의하면 된다.
캐시 역시 메모리 누수를 일으키는 주범이다. 재사용할 목적으로 객체의 레퍼런스를 캐시에 넣어두지만, 언제 이 캐시가 유효한지 정확히 정의하기 어렵기 때문에 객체를 다 쓴 뒤에도 비우는 것을 잊기 쉽다. 외부에서 키를 참조하는 동안만 엔트리가 살아 있다고 가정하면 WeakHashMap 을 사용하는 것도 나쁘지 않다. 캐시의 키에 대한 레퍼런스가 캐시 밖에 서 필요 없어지만 자동으로 제거되는 원리이다.
그런데 보통은 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다. 백그라운드 스레드를 통해 쓰지 않는 엔트리를 청소하거나 캐시에 새 엔트리를 추가할 때 엔트리를 검사하여 청소하는 방법이 있다. LinkedHashMap 은 removeEldestEntry 메서드가 후자의 방식으로 처리한다.
리스너와 콜백도 메모리 누수를 일으킬 수 있다. 콜백을 등록하기만 하고 명확히 해지하지 않는다면 계속 쌓이기만 할 것이다. 이럴 때 콜백을 약한 참조로 저장하면 GC 가 수거할 수 있다. WeakHashMap 에 키로 저장하는 방법도 한 예이다.
Weak 레퍼런스에 대한 자세한 내용은 아래 링크를 참조하자.
"다 쓴 객체 참조를 살려두면 GC 는 그 객체 뿐만 아니라 그 객체를 참조하는 모든 객체를 회수해가지 못 한다.
GC 의 편리성에 속아 성능을 잃지 말자."
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 9 "try-with-resource 를 사용하라" (0) | 2022.04.29 |
---|---|
[Effective JAVA] 8 "finalizer 와 cleaner 사용을 피하라" (0) | 2022.04.29 |
[Effective JAVA] 6 "불필요한 객체 생성을 피하라" (0) | 2022.04.28 |
[Effective JAVA] 5 "자원을 직접 명시하지 말고 의존 객체 주입을 사용하라" (0) | 2022.04.27 |
[Effective JAVA] 4 "인스턴스화를 막으려거든 private 생성자를 사용하라" (0) | 2022.04.26 |