728x90
반응형
[아이템 7] 다 쓴 객체 참조를 해제하라
메모리 관리
- 자바처럼 가비지 컬렉터를 갖춘 언어를 사용하면 다 쓴 객체를 알아서 회수해주기 때문에 훨씬 편해집니다.
- 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 사실이 아닙니다.
메모리 누수가 일어나는 위치는 어디인가?
다음은 스택을 간단히 구현한 코드입니다.
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) {
esureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
public void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
- 코드 상에서는 특별한 문제는 없습니다(제네릭 버전은 아이템 29 참조).
- '메모리 누수'로 인해, 이 프로그램을 오래 실행하다 보면 가비지 컬렉션 활동과 메모리 사용량이 늘어나 성능이 저하될 것입니다.
- 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료되기도 합니다.
- 위 코드에서 메모리 누수는 어디에서 일어날까?
- 스택 크기의 변동에도 불구하고, 미래에 사용되지 않은 참조가 스택에 남아있어, 가비지 컬렉터가 이를 회수하지 못합니다.
- '활성 영역' 이외의 영역에 위치한 참조들이 모두 '사용하지 않을 참조'에 해당하며, '활성 영역'은 size보다 작은 인덱스의 원소들로 구성됩니다.
메모리 누수와 가비지 컬렉션 : 스택에서 미사용 참조의 영향
- 가비지 컬렉션 언어에서는 의도치 않은 객체 참조로 인한 메모리 누수 발견이 매우 어렵습니다.
- 하나의 객체 참조가 존재한다면, 그 객체는 물론이고 해당 객체가 참조하는 모든 객체들을 가비지 컬렉터가 회수하지 못하게 됩니다.
- 이로 인해 몇 개의 객체가 다수의 객체의 회수를 방해하며, 이는 잠재적으로 프로그램의 성능을 저하시킬 수 있습니다.
해법 : 해당 참조를 다 썻을 때 null 처리
예시의 스택 클래스에서는 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 깨너질 때입니다.
다음은 pop 메서드를 제대로 구현한 모습입니다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] == null; // 다 쓴 참조 해제
return result;
}
- 약 null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료됩니다.
- 위 문제로 프로그래머는 모든 객체에 일일이 null 처리를 하게되는데, 이는 프로그램을 필요 이상으로 더럽게 만들 뿐 입니다.
- 객체 참조를 null 처리하는 일은 예외적인 경우여야 합니다.
- 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것입니다.
null 처리는 언제?
- 스택은 자기 메모리를 직접 관리하기 때문에 메모리 누수에 취약합니다.
- 이 스택은 elements 배열로 저장소 풀을 만들어 원소들을 관리합니다.
- 배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않습니다.
프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 합니다.
일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항상 메모리 누수에 주의해야 합니다.
원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 합니다.
캐시 : 메모리 누수의 주범
- 캐시 역시 메모리 누수를 일으키는 주범입니다.
- 객체 참조를 캐시에 넣고 나서, 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있습니다.
캐시 : 해답
- 운 좋게 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라 WeakHashMap을 사용해 캐시를 만들면 됩니다. 다 쓴 엔트리는 그 즉시 자동으로 제거될 것입니다.
- 단, WeakHashMap은 이러한 상황에서만 유용하다는 사실을 기억해야 합니다.
- 캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용합니다.
- 이런 방식에서는 쓰지 않는 엔트리를 청소해줘야 합니다.
- (Scheduled ThreadPoolExecutor 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있습니다.
- LinkedHashMap은 remove EldestEntry 메서드를 써서 후자의 방식으로 처리합니다.
- 더 복잡한 캐시를 만들고 싶다면, java.lang.ref 패키지를 직접 활용해야 할 것입니다.
리스터(listener), 콜백(callback) : 메모리 누수의 주범
- 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 조치해주지 않는 한 콜백은 계속 쌓여갈 것입니다.
- 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해갑니다.
- 예를 들어 WeakHashMap에 키로 저장하면 됩니다.
결론
- 메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있습니다.
- 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 합니다.
- 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요합니다.
728x90
반응형
'Study > Effective Java[이펙티브 자바]' 카테고리의 다른 글
[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.06.14 |
---|---|
[아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2023.06.11 |
[아이템 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2023.06.07 |
[아이템 2] 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2023.06.07 |