자바는 C와 C++과는 다르게 가비지 컬렉터를 갖추고 있어서 메모리를 관리하지 않아도 된다.
그렇다고 메모리 관리에 신경쓰지 않아도 된다는 것은 아니다. 이것은 오해이다.
메모리 누수가 일어나고 있는 코드
public class MemoryLeakTest {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public MemoryLeakTest() {
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);
}
}
코드를 잘 보면 문제가 없어보이지만, 클래스의 크기가 커졌다가, 줄어들었을 때 클래스에서 꺼내진 객체들을 가비지 컬렉터가 회수 하지 않게됩니다. 그 객체들을 더 이상 사용하지 않더라도 그렇습니다.
왜냐하면 이 클래스는 객체들의 다 쓴 참조를 여전히 가지고 있기 때문입니다. 다 쓴 참조라는 의미는 문자 그대로 앞으로 다시 쓰지 않을 참조를 뜻합니다.
객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못합니다.
public class MemoryLeakTest {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public MemoryLeakTest() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity() {
if(elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
pop() 부분에 다쓴 부분을 null 처리하는 코드를 추가해주었습니다. 이렇게 되면 메모리 누수도 발생하지 않고, 만약 null 처리한 참조를 실수로 사용하려 한다면 NullPointerException을 던지기 때문에 종료됩니다.
그렇다고 모든 부분에서 객체를 null처리하는 것은 좋지 않습니다.
배열 비활성 영역 처리
배열은 활성 영역과 비활성 영역이 존재하고, 비활성 영역도 똑같이 유효한 객체이므로 가비지 컬렉터가 정리를 하지 않습니다. 그렇기 때문에 이부분에서 메모리 누수가 발생할 수 있습니다.
그렇기 때문에 배열에서 비활성 영역이 생기는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에게 알려야합니다.
캐시도 메모리 누수를 일으키는 주범
캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용합니다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해줘야 합니다. 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있습니다.
리스너 혹은 콜백도 메모리 누수를 일으키는 주범
클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 처리하지 않은 콜백은 계속 쌓일 것입니다. 그렇기 때문에 이럴 때는 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해 갑니다. 예를 들어 WeakHashMap에 키로 저장하는 방법이 있습니다.
마무리
자바는 메모리 관리를 가비지 컬렉터가 해주기 때문에 어떤 원리로 동작하는지
잘 이해하고 있어야 더 편하게 메모리를 사용할 수 있을 것입니다.
예를 통해 많은 경우들을 알게되었는데 실제로 프로젝트를 할 때,
발생할 수 있는 문제들도 있을 것입니다. 미리 학습하여 프로젝트 시
다음과 같은 문제가 생기지 않도록 신경쓰고 코딩할 것입니다.
포스팅 읽어주셔서 감사합니다.
[출처]
이펙티브 자바 Effective Java 3/E - 조슈아 블로크 저/ 개앞맵시 역
'Book Record > Effective Java 3E' 카테고리의 다른 글
[Effective Java] equals는 일반 규약을 지켜 재정의하고 hashCode도 재정의하라 (1) | 2022.12.02 |
---|---|
[Effective Java] try-finally 보다는 try-with-resources를 사용하라 (1) | 2022.12.01 |
[Effective Java] 불필요한 객체 생성을 피하라 (0) | 2022.12.01 |
[Effective Java] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.12.01 |
[Effective Java] private 생성자나 열거타입으로 싱글턴임을 보증하라 (2) | 2022.12.01 |