Item07. 다 쓴 객체 참조를 해제하라!
- C, C++ 처럼 메모리를 직접 관리해야 하는 언어와 비교해 자바는 가비지 컬렉터를 통한 편리함을 느낄 수 있다.
- 그렇다면 자바 사용 시 메모리 관리에 소홀해도 되는가? 그건 아니다.
메모리 누수
메모리 누수의 첫번째 주범 : 스택
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
32
33
34
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 Stack(int size) {
this.elements = new Object[size];
}
public void push(final Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
return null;
}
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보
* 배열 크기를 늘려야 할 때마다 2배씩 늘림
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, (size * 2) + 1);
}
}
- 위 코드는 특별한 문제가 없어 보이지만 숨은 문제가 있다. 바로 “**메모리 누수**“다.
- 이 스택을 사용하는 프로그램을 오래 실행한다면 점차 가비지 컬렉터의 활동과 메모리 사용량이 증가하게 된다.
- 심한 경우에는 디스크 페이징 또는 OOM 에러를 일으켜 종료된다.
페이징
프로그램 중 자주 사용되지 않는 부분의 작업 메모리를 주기억장지(메모리) -> 보조기억장치(HDD,SSD)로 옮기는 방식을 통해 메모리 관리하는 기법
메모리누수
가비지 컬렉터에 의해 메모리가 정리되지 않고, 프로그램이 계속해서 점유하고 있는 현상
메모리 누수는 어디서 일어날까?
- 위 코드에서는 스택이 커졌다가 줄어들 때 꺼내진 객체를 가비지 컬렉터가 회수하지 않아 누수가 생긴다.
사용하지도 않는 객체인데 왜 회수하지 않는가? 스택이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.
- 위 코드를 기준으로 elements 배열의 ‘활성 영역’ 밖 참조들이 다시는 사용하지 않을 다 쓴 참조에 해당한다.
가비지 컬렉터의 동작
- 가비지 컬렉션 언어 -> 메모리 누수를 찾기가 아주 어렵다.
- 왜? 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체 뿐만 아니라 그 객체가 참조하는 모든 객체를 연쇄적으로 살려두기 때문이다.
- 이는 단 몇개의 객체가 수많은 객체의 회수를 막는 사태를 일으킬 수 있다.
해법? 해당 참조를 다 썼을 때 null 처리해주는 것이다.
- 위 스택 코드를 기준으로 보면 참조가 더 이상 필요 없게 되는 부분은 pop() 메서드가 동작할 때이다.
1
2
3
4
5
6
7
// 제대로 구현한 pop 메서드
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // obsolete reference
return result;
}
다 쓴 참조를 null 처리해줄 때의 득
- null 처리한 참조를 실수로 사용하려고 할 때 NullPointerException을 던져 프로그램 오류를 조기에 해결할 수 있다.
다 쓴 참조를 null 처리해줄 때의 실
- 프로그램이 필요 이상으로 지저분해질 수 있다.
- 그렇기 때문에 객체 참조를 null 처리해주는 일은 예외적인 경우에 한해 진행하면 좋다.
- 일반적으로는 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 방법을 사용하는 것이 좋다.
왜 Stack 클래스는 메모리 누수에 취약한가?
- 스택이 자기 자신의 메모리를 직접 관리하기 때문이다.
- 스텍은 객체 자체가 아니라 객체 참조를 담는 배열 풀을 만들어 원소를 관리한다.
- 이 배열의 원소 중 활성 영역에 속한 원소만이 사용되지만 가비지 컬렉터는 이 사실을 알 수가 없다.
- 그렇기에 프로그래머는 객체의 활용 여부를 가비지 컬렉터에 명시해줘야한다. 이 과정에서 null 처리가 필요하다.
메모리 누수의 두번째 주범 : 캐시
- 객체 참조를 캐시에 넣고 나서, 그 사실을 잊고 한참을 놔두는 일이 빈번하다.
- 이러한 경우 메모리 누수가 일어난다.
캐시로 인한 메모리 누수의 해결책
- 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시를 사용하는 경우 -> WeakHashMap을 사용해 캐시를 만든다. 그럼 다 쓴 엔트리는 자동적으로 제거된다.
- 엔트리의 유효기간을 설정한다. 주로 엔트리가 생성되고 시간이 지날수록 가치를 하락시키는 방식을 사용한다. 가치 하락이 많이 일어난 사용하지 않는 엔트리는 이따금 청소해줘야 한다.
메모리 누수의 세번째 주범 : 리스터 혹은 콜백 호출
- 클라이언트가 콜백을 호출하고 명백히 해지하지 않는다면 콜백이 쌓여 누수가 발생한다.
- 이러한 경우 콜백을 약한 참조(weak reference)로 저장해 가비지 컬렉터가 즉시 수거하도록 구성할 수 있다.
추가 정리
- 대표적 사례 외에도 메모리 누수가 발생하는 사례들이 있다.
- 이러한 경우는 코드 리뷰나 힙 프로파일러 같은 디버거 사용을 통해 발견하고 해결할 수 있도록 경험을 쌓아야 한다.
Comments powered by Disqus.