메모리 누수가 일어나는 예
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 Object pop() { if(size == 0) throw new EmptyStackException(); return elements[--size]; } }
- 위의 pop에서 elements의 범위만을 size로 줄이기에 size 보다 큰 범위에 있는 elements 참조들은 해당 객체를 계속 참조하고 있기에 GC가 일어나지 않음 ⇒ 메모리 누수
- 가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다로움. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체 뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체..)를 회수해 가지 못함
해법 : 해당 참조를 다 썼을 때 null처리 (참조 해제)하면 됨
public Object pop(){ if(size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 다 쓴 참조 해제 return result; }
- 그러나 객체 참조를 null 처리하는 일은 예외적인 경우여야 함
- 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(변수의 scope) 밖으로 밀어내는 것임 → 변수의 범위를 최소가 되게 정의하기(아이템 57)
메모리 누수에 취약한 경우
자기 메모리를 직접 관리하는 클래스
- 일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 함
캐시
- 객체 참조를 캐시에 넣고 나서, 이 사실을 까맣게 잊은 채 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있음
- 해법 중 하나 : WeakHashMap을 사용하기. 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있도록 하게 — https://blog.breakingthat.com/2018/08/26/java-collection-map-weakhashmap/
- 캐쉬를 만들 때 보통은 캐쉬 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용함. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해 주어야 한다.
- (ScheduledThreadPoolExecutor 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수작업으로 수행하는 방법이 있음
- LinkedHashMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리함(캐시에 새 엔트리를 추가할 때 부수작업으로 수행)
리스너 혹은 콜백
- 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것임
- 이럴 때 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어 WeakHashMap에 키로 저장하면 됨