메모리 누수를 주의해야 한다.
자바에서는 가비지 컬렉터가 다쓴 객체를 알아서 회수해간다 하지만 그렇다고해서 메모리 관리에 신경쓰지 않으면 안된다. 메모리 누수가 발생하는 프로그램을 오래 실행하다보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하되거나 메모리초과 오류(OufOfMemoryError
) 가 발생할 수 있다.
[메모리 누수가 일어나는 스택 구현]
public class StackMemoryLeak {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity(e);
elements[size++] = e;
}
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
// pop을 하지만 elements는 초기화되지 못하고 elements[size]를 여전히 참조하게 됨
return elements[size--];
}
private void ensureCapacity(final Object e) {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
- 위와 같은 스택은
elements
배열이 사라지지 않고 메모리에 계속 누적되며 객체의 참조를 가지고 있게 되므로 가비지 컬렉터가 작동하지 못한다.- 스택이 그 객체들의 다 쓴 참조
(obsolete reference)
를 여전히 가지고 있게 된다는 뜻 - elements 배열의
활성 영역
밖의 참조들이 모두 다 쓴 참조에 해당된다.활성 영역
:인덱스가 size보다 작은 원소들을 뜻함
- 스택이 그 객체들의 다 쓴 참조
- 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다
메모리 누수 방지 방법
- 가장 간단한 방법은 해당 참조를 다 사용했을 때 변수를 null로 선언(참조 해제) 해버리는 것이다
- 그럼 더 이상 Heap 영역에 저장된 객체에 대한 참조가 존재하지 않기 때문에 가비지 컬렉터가 작동할 수 있다
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
허나 모든 객체를 다 쓰자마자 null
처리하는 것은 별로 바람직하지 않다.
객체 참조를 null
처리하는 일은 예외적인 경우여야 하며 가장 마지막에 고려되어야한다.
일반적으로 다 쓴 참조를 해제하는 가장 좋은 방법은 해당 변수를 scope (유효 범위) 밖으로 밀어내는 것이다.
그렇다면 null처리는 언제해야 할까?
- 예시로 든
Stack
클래스처럼 자기 메모리를 직접 관리하는 경우에 메모리 누수에 취약해진다.- 객체 자체가 아니라 객체 참조를 담는
elements
배열로 메모리를 관리한다
- 객체 자체가 아니라 객체 참조를 담는
- 이러한 경우 프로그래머는 비활성 영역이 되는 순간
null
처리를 통해 해당 객체가 더 이상 쓰이지 않는다는 사실을 가비지 컬렉터에게 알려주어야 한다.
추가적으로 주의해야 할 메모리 누수 사례
1. 캐시 메모리 누수
객체 참조를 캐시에 넣어두고 이를 방치하는 일이 자주 발생한다. 이를 해결하는 방법에는 여러가지가 있다.
WeakHashMap
을 사용해 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시를 사용하는 방식.더보기WeakHashMap[WeakHashMap]
일반적인 HashMap의 경우 Key와 Value가 put되면 사용여부에 관계없이 해당 내용은 제거되지 않는다. 하지만 weakHashMap의 경우 Key에 해당하는 어떤 객체가 null이 되면 해당 객체를 key로 하는 HashMap의 Element도 더이상 사용하지 않는다고 판단되어 자동으로 제거되어 버린다.
ScheduledThreadPoolExecutor
같은 백그라운드 스레드를 활용해 엔트리를 청소하는 방식.LinkedHashMap
의removeEldesEntry
메서드를 활용해 엔트리를 청소하는 방식.java.lang.ref
패키지를 이용해 더 복잡한 캐시를 만드는 방법.
2. 리스너(listener) 혹은 콜백(callback)
클라이언트가 콜백을 등록만하고 명확히 해지하지 않는다면, 콜백은 계속 쌓이게 된다.
이럴 때 콜백을 약한 참조(weak reference
)로 저장하면 가비지 컬렉터 가 즉시 수거해 간다.
ex) WeakHashMap
에 키로 저장
정리
- 메모리 누수는 겉으로 잘 드러나지 않기 때문에 이런 종류의 문제는 예방법을 익혀두는것이 중요하다. 그렇지 않으면 디스크 페이징이나
OutOfMemoryError
를 일으켜 프로그램이 예기치 않게 종료될 수 있다. - 메모리누수를 방지하는 방법으로 다 쓴 참조 객체를
null
처리 해주는 방법이 있다. 하지만, 사실 모든 객체를 다 사용 후null
처리하는것은 별로 바람직하지 않다.- 위에서 본 스택의 경우 스택에서 꺼내진 객체들에 대한 다 쓴 참조를 여전히 스택이 가지고 있다. 꺼내진 객체들은 비활성영역에 해당하는 객체들인데 가비지 컬렉터는 이런 비활성영역에 대한 객체들을 회수하지 못한다. 이처럼 자기 메모리를 직접 관리하는 클래스의 경우 참조한 객체를 사용 후
null
처리를 통해서 객체가 사용하지 않는다는 사실을 가비지 컬렉터에게 알리고 메모리 누수를 방지하자.
- 위에서 본 스택의 경우 스택에서 꺼내진 객체들에 대한 다 쓴 참조를 여전히 스택이 가지고 있다. 꺼내진 객체들은 비활성영역에 해당하는 객체들인데 가비지 컬렉터는 이런 비활성영역에 대한 객체들을 회수하지 못한다. 이처럼 자기 메모리를 직접 관리하는 클래스의 경우 참조한 객체를 사용 후
- 다 사용한 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수의 범위를 최소한으로 지정하여 사용 후 유효범위 밖으로 자연스레 밀려나게 하는 것이다.
'Backend > Java' 카테고리의 다른 글
[Java] 정렬 알고리즘 정리 (1) | 2025.07.02 |
---|---|
[EffectiveJava] Item 14. Comparable을 구현할지 고려하라 (1) | 2025.07.01 |
[EffectiveJava] Item 55. 옵셔널 반환은 신중히 하라 (1) | 2025.07.01 |
[EffectiveJava] item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라 (0) | 2025.07.01 |
[EffectiveJava] item 64. 객체는 클래스가 아닌 인터페이스로 참조하라 (0) | 2025.07.01 |