아이템7. 다 쓴 객체 참조를 해제하라.
자바는 C, C++처럼 메모리를 직접 관리하는 언어와 다르게 GC에 의해 메모리를 관리한다.
GC가 메모리 관리를 해주지만, 메모리 관리에 신경 써야한다.
스택을 간단히 구현한 예제 코드를 살펴보자
Stack을 간단히 구현한 코드
public class Stack {
private Object[] elements;
private int size;
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()에서 메모리 누수 문제가 발생한다.
- 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들은 사용하지 않더라도 GC가 회수하지 않는다.
- 즉 다 쓴 참조(obsolete reference)를 갖고 있는다.
- 다 쓴 참조란, 앞으로 다시는 쓰지 않을 참조를 의미한다. 위의 코드에서는 elemets 배열의 '활성 영역'밖의 참조들이 모두 여기(다 쓴 참조)에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.
- GC는 메모리 누수를 찾기가 까다롭다. 그 이유는 객체 참조를 하나 살려두면, 그 객체가 참조하는 모든 객체를 회수해가지 못한다.
다 쓴 참조 객체를 회수하는 방법
다 쓴 참조 객체를 회수하는 방법으로는 null처리, 캐시 엔트리 처리, 콜백 처리 등이 있다.
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 객체 회수
return result;
}
다음과 같이 참조를 사용 후 null처리를 통해 참조를 해제하면 해결된다. 또한 해당 처리로 인해 실수로 null인 인덱스에 참조하게 되면 NPE로 오류를 인지하기 수월해진다.
하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.
그렇다면 null 처리는 언제하면 좋을까?
바로 메모리를 직접 관리하는 경우이다. 위의 스택 예제는 elements를 배열로 저장소 풀을 만들어 원소를 관리한다.
문제는 이 사실을 GC가 알 수 없다. GC의 입장에선 현재 elements의 배열 원소 중 활성 영역과 비활성 영역 모두 똑같은 유효 객체로 판단히가 때문이다.
따라서 메모리를 직접 관리하는 경우 비활성 영역이 되는 순간 null 처리를 해서 해당 객체를 사용하지 않음을 GC에게 알려야 한다.
캐시 역시 메모리 누수의 주범이다. 캐시를 만들 때 엔트리(키 값)의 유효기간을 정확하게 정의하기 어렵다. 따라서 흔히 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다. 이런 방식에는 백그라운드 스레드를 활용하는 ScheduledThreadPoolExecutor 나 새 엔트리를 추가할 때 부수 작업으로 수행하는 LinkedHashMap의 removeEldesEntry()를 사용한다. 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 한다.
메모리 누수의 세 번째 주범은 바로 listener 혹은 callback 이다. 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 계속 쌓일 것이다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 GC가 즉시 수거해간다.
예를 들어 WeakHashMap에 키로 저장하면 된다.
메모리 누수는 겉으로 잘 드러나지 않아서 시스템에 수년간 잠복하는 사례도 있다. 이런 누구는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야 발견되기도 한다. 따라서 예방법을 익혀두는 것을 중요하다.
'도서 > 정리' 카테고리의 다른 글
[이펙티브 자바] 아이템15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.07.05 |
---|---|
[이펙티브 자바] 아이템9. try-finally보다는 try-with-resources를사용하라 (0) | 2023.07.04 |
[이펙티브 자바] 아이템6. 불필요한 객체 생성을 피하라 (0) | 2023.05.15 |
[이펙티브 자바] 아이템5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.05.09 |
[이펙티브 자바] 아이템4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (1) | 2023.05.07 |