2022. 3. 16. 19:18ㆍJava/Effective Java
다 쓴 객체 참조를 해제하라
C같은 메모리를 직접 관리 하는 언어에서 자바와 같이 Garbage Collector를 가진 언어를 사용하면 편리성은 증대해진다.
Garbage Collecotor가 사용한 객체를 회수하기 때문이다. 그렇다고 메모리 관리에 주의를 기울이지 않으면 안된다.
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'와 같은 오류를 일으키게 된다.
- Disk Paging
메인 메모리 상에서 사용하기 위해 보조 저장 장치에 데이터를 저장하고 검색하는 메모리 관리 체계이다.
페이징은 최신 운영 체제에서 가상 메모리 구현의 중요한 부분으로, 보조 저장 장치를 사용하여 프로그램이 사용 가능한 물리적 메모리 크기를 초과할 수 있다.
스택에서 누수는 어디서 발생할까?
ensureCapacity() 메소드에서 메모리 누수가 발생한다.
스택의 크기를 키우고 줄일때 버려지는 객체들은 Garbage Collector가 회수하지 않기 때문에 계속 가지고 있게 된다.
저렇게 버려진 객체들은 Obsolete reference라고 불린다.
어떻게 해야할까?
Garbage Collector는 사용하지 않는 객체라도 하나가 살아있다면 그 객체뿐만 아니라 참조하는 모든 객체는 회수되지 않는다. 이는 잠재적으로 보았을때 메모리나 코드 성능에 악영향을 준다.
해당 참조를 사용하였을 경우 null 처리를 통해 참조 해제 시켜주는 것이 좋다.
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object elements[--size];
elements[size] = null;
return result;
위와 같이 elements 객체를 null 처리 시키면 된다. null을 이용하면 다른 이점이 실수로 사용할 경우 NullPointerException을 던져 잘못 된 일을 처리한다. 하지만 매번 처리해야하는 객체를 null 처리 해주는 것은 코드를 더럽힌다. 이를 사용할 때는 예외적인 경우여야 한다.
변수를 Scope 밖으로 밀자
스택은 자기 메모리를 직접 관리하기 때문에 메모리 누수에 약할 수 밖에 없다. elements 배열로 저장소 풀을 만들어 관리해야 한다. 하지만 Garbage Collector는 배열에 존재하는 비활성 영역을 알지 못 한다.
아는 것은 프로그래머 자신 뿐이다. 이때, 비활성 영역도 활성 영역으로 인식하여 메모리 낭비를 하게 된다.
이때는 null 처리를 통해 Collector에게 알려야 한다.
캐시
캐시 또한 메모리 누수를 일으킨다.
이때는 외부에서 키를 참조하는 동안만 활성화 되어 있는 캐시를 사용해야 하는 상황이면 WeakHashMap을 사용해 캐시를 만들어야 한다. 사용한 캐시는 즉시 삭제된다.
물론 WakHashMap은 위와 같은 상황에서만 유용하다. ScheduledThreadPoolExecutor와 같다. 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다. LinkedHashMap은 removeEldestEntry 메소드를 써서 처리한다.
Listener & Callback
콜백은 등록만 하고 명확히 해지하지 않는다면 무너가 조치해주지 않는 콜백은 쌓인다.
Weak Reference로 지정하면 즉시 수거한다. 예를 들면 WeakHashMap에 키로 저장하면 된다.
핵심 정리
메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수 년간 잠복한다. 철저한 코드 리뷰는 힙 프로파일러 같은 디버깅 도구를 동원야만 발견되기도 한다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 아이템 9 : try-finally 보다는 try-with-resources를 사용하라 (0) | 2022.03.31 |
---|---|
[Effective Java] 아이템 8 : finalizer와 cleaner 사용을 피하라 (0) | 2022.03.30 |
[Effective Java] 아이템 6 : 불필요한 객체 생성을 피하라 (0) | 2022.03.16 |
[Effective Java] 아이템 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.03.15 |
[Effective Java] 아이템 4 : 인스턴스화를 막으려거든 Private 생성자를 사용해라 (0) | 2022.03.14 |