다 쓴 객체 참조를 해제하라
Stack 코드의 예제
public 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 e) {
ensureCapacity();
elements[size++] = e;
}
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);
}
}
- 위의 평범한 스택 코드는 사실 메모리 누수를 발생시킨다.
pop()
에서elements[size] = null
을 해주지 않으면, 해당element
가 활성 영역에서 벗어나더라도 JVM은 그를 인식하지 못한다.- 가비지 컬렉션 언어에서는 메모리 누수를 찾기가 생각보다 까다롭다.
- 객체 참조 하나가 살아있다면, 그 객체가 참조하는 다른 모든 객체도 살아있게 되기 때문이다.
- 단 하나의 객체가 수많은 다른 객체들을 가비지 컬렉터에 의해 회수되지 못하도록 할 수 있다.
메모리 누수 해결하기
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[size];
elements[size] = null;
return result;
}
- 위의 방식으로
pop()
메서드를 구현하면,elements[size]
는 더이상 활성 영역이 아닌 객체를 가리키지 않는다. - 이제 반환된
Object
는Stack
을 사용하는 클라이언트에서 쓰이다가 더이상 참조되지 않을 때 가비지컬렉터에 의해 회수될 것이다. - 다 쓴 참조를
null
처리 하면, 의도치 않게 활성 영역이 아닌 객체를 참조하려 했을 때NullPointerException
을 띄워주는 이점도 존재한다.- 프로그램 오류는 가능한 조기에 발견하는 것이 좋다.
객체의 null
처리는 어떻게 코딩하는 것이 바람직할까?
- 사실 객체 참조를
null
처리 하는 일은 예외적인 경우여야 한다. - 메모리 누수를 예방할 수 있는 가장 좋은 방법은 최소한의 스코프에서 변수를 이용하고 버리는 것이다.
- 사용하지 않는 객체를 참조하는 변수는 자연스레 스코프 밖으로 밀려나 가비지 컬렉터에 의해 처리될 것이다.
그렇다면 Stack
클래스는 왜 메모리 누수가 발생하게 되었을까?
- 메모리를 직접 관리하기 때문이다.
- 동적으로 용량이 변하는 배열을 직접 만들어 관리하고 있다.
- 이 경우에 JVM은 스택이 사용하는
elements
배열이 참조하는 객체들이 어디까지 활성 영역인지 알 수 없다.
자기 메모리를 직접 관리하는 클래스는 항상 메모리 누수에 주의해야 한다.
메모리 누수의 주범들
캐시도 메모리 누수의 주범이 될 수 있으니 주의해야 한다.
- 보통 캐시를 구현하면,
Map
자료구조 형태로 구현하는 일이 흔하다.Map
의key
가 객체를 참조하고 있다면, 해당 객체와 해당 객체가 참조하는 모든 객체는 계속 살아있는 상태가 될 것이다.WeakHashMap
을 사용하면 이런 문제를 일부 해결할 수 있다.WeakHashMap
에서는 내부적으로Entry
가WeakReference
를 상속하여 구현되기 때문에key
를null
처리 하는 순간Entry
의key
가 가비지 컬렉터에 의해 회수된다.
Map
에서 사용하지 않는Entry
는 종종 청소해주는 것이 좋다.ScheduledThreadPoolExecutor
와 같은 백그라운드 스레드를 활용하거나, 새 엔트리를 추가할 때 부수작업을 추가해도 된다.- 복잡한 최적화 캐시를 만들고 싶다면,
WeakHashMap
처럼java.leng.ref
를 직접 이용해도 된다.
콜백과 리스너도 메모리 누수의 주범이 될 수 있다.
- 클라이언트가 콜백 등록 후 명시적으로 해지하지 않으면, 콜백이 계속 쌓일 수 있다.
- 콜백을 애초에
WeakReference
로 사용하면, 가비지 컬렉터가 이를 즉시 수거해갈 것이다.
핵심 정리
- 메모리를 직접 관리하는 클래스를 만들 때는 JVM에서 인지할 수 있게
null
처리를 명시적으로 해주자. - 캐시, 콜백 등을 사용할 때는
WeakHashMap
,WeakReference
를 이용하여 메모리 누수를 막자
메모리 누수는 일어나도 별다른 버그나 에러가 없는 경우가 많기에 간과하기 쉽다. 철저한 코드 리뷰나 힙 메모리 분석을 통해서야 마침내 발견되기도 한다. 예방법을 익혀두어 미연에 방지하는 것이 좋다.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item9. try-finally보다는 try-with-resources를 사용하라 (0) | 2021.12.28 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라 (0) | 2021.12.27 |
이펙티브 자바, 쉽게 정리하기 - item6. 불필요한 객체 생성을 피하라 (0) | 2021.12.26 |
이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.12.25 |
이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라 (0) | 2021.12.25 |