반응형
가비지 컬렉터가 있더라도 메모리 관리를 해야 한다
[예시] 스택을 간단히 구현한 코드
메모리 누수가 일어나는 위치는 어디인가?
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);
}
}
}
문제가 없어 보이지만 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다.
상대적으로 드문 경우긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError
를 일으켜 프로그램이 예기치 않게 종료되기도 한다.
메모리 누수는 어디서 일어나는가?
- 이 코드에서 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
(프로그램에서 그 객체들을 더 이상 사용하지 않더라도) - 이유: 이 스택이 그 객체들의 다 쓴 참조(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
처리하는 일은 예외적인 경우여야 한다. - 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것
- 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이뤄진다.
그렇다면 null 처리는 언제 해야 할까?
Stack
클래스가 메모리 누수에 취약한 이유는 스택이 자기 메모리를 직접 관리하기 때문이다.
위 예시의 스택은 객체 자체가 아니라 객체 참조를 담는 elements 배열로 저장소 풀을 만들어 원소들을 관리한다.
배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다.
하지만 가비지 컬렉터는 이 사실을 알 수 없고 가비지 컬렉터가 보기에는 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체다.
그러므로 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.
- 캐시 또한 메모리 누수를 일으키는 주범
- 객체 참조를 캐시에 넣고 그대로 두는 경우가 많다.
- 해법 1. 운 좋게 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트리가 살아 있는 캐시가 필요한 상황이라면
WeakHashMap
을 사용해 캐시를 만들어라. 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다. 단WeakHashMap
은 이러한 상황에서만 유용하다. - 해법 2. 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어려워 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서 쓰지 않는 엔트리를 청소해줘야 하는데 (Scheduled ThreadPoolExecutor 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다.
LinkedHashMap
은remove EldestEntry
메서드를 사용하여 후자의 방식으로 처리한다. 더 복잡한 캐시를 만들고 싶다면java.lang.ref
패키지를 활용해야 한다. - 해법 3. 리스너(listener) 혹은 콜백(callback). 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 조치가 없는 한 콜백은 계속 쌓여간다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다.
WeakHashMap
에 키로 저장하면 된다.
- 해법 1. 운 좋게 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트리가 살아 있는 캐시가 필요한 상황이라면
반응형
'Review > Book' 카테고리의 다른 글
[Effective Java 3/E] 6. 불필요한 객체 생성을 피해라 (1) | 2023.10.30 |
---|---|
[Effective Java 3/E] 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.10.30 |
[Effective Java 3/E] 4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.10.25 |
[Effective Java 3/E] 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2023.10.25 |
[Effective Java 3/E] 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (1) | 2023.10.24 |