객체 생성과 파괴(Item 7)
Item 7. 다 쓴 객체 참조를 해제하라
자바는 가비지 컬렉터를 사용하기 때문에 메모리 자원을 직접 해제하지는 않는다. 중간중간 가비지 컬렉터가 알아서 필요 없는 자원들을 메모리에서 삭제해주기 때문이다. 그러나, 가비지 컬렉터가 삭제할 수 없는 인스턴스가 있는데 그것은 바로 프로그램 내에서 참조되고 있는 인스턴스이다. 가비지 컬렉터는 애초에 참조가 하나도 없는 인스턴스를 제거해주기 때문에 하나라도 참조되어 있으면 제거하지 않는다.
즉, 가비지 컬렉터가 인스턴스를 잘 관리할 수 있게 도와주는 것은 개발자의 역량이라고 볼 수 있겠다. 인스턴스를 메모리에서 직접 제거할 수는 없지만 가비지 컬렉터가 잘 제거할 수 있도록 도와주는 것은 가능하다. 이것은 꽤나 중요한 작업으로 불필요한 인스턴스들이 메모리를 차지하고 있게 되면 정작 필요한 인스턴스의 공간이 부족할 수 있기 때문이다.
그렇다면 어떤 방식으로 참조를 잘 해제할 수 있을까? 책에서는 3가지 정도의 방법을 소개하고 있다.
참조 해제
유효 범위 밖으로 밀어내기
가장 쉽고 간단하게 구현하는 방법은 해당 참조가 자연스럽게 없어지게끔 하는 것이다. 이미 우리가 의도하지 않아도 가장 많이 사용하고 있는 방법으로 해당 참조를 포함하는 메서드가 끝나게 되면 변수가 메모리에서 사라지게 되고 자연스럽게 해당 인스턴스를 참조하던 것도 사라지게 된다.
명시적으로 null 할당하기
이따금 메모리를 직접 관리하는 클래스들이 있는데 이런 클래스를 다룰 때 메모리가 누수되지 않도록 잘 관리해 주어야 한다.
아래 예시를 살펴보자.
Stack
자바에서 기본으로 제공하는 Stack
클래스는 Object[]
배열을 통해 자신이 담고 있을 객체들을 직접 관리한다.
pop()
메서드를 통해 가장 나중에 들어온 인스턴스를 제거하면서 반환하는데 이 로직을 자세히 살펴보자.
public synchronized void removeElementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
modCount++;
elementCount--;
elementData[elementCount] = null; /* to let gc do its work */
}
맨 마지막 줄에 elementData[elementCount] = null;
코드를 통해 객체 참조를 명시적으로 해제하는 것을 볼 수 있다.
옆에 주석을 보면 “GC가 동작하도록 하기 위해서”라고 적혀있는데(내가 작성한 주석이 아니라 자바 소스에 적혀 있는 주석이다)
저렇게 참조를 해제하지 않으면 Object[]
배열에 해당 인스턴스가 계속 남아있게 되어 가비지 컬렉터가 인스턴스를 수거해 가지 않기 때문이다.
WeakReference 사용하기
또 다른 예제를 살펴보자. 아래의 PostRepository
클래스는 cache
필드를 통해 객체를 캐싱하고 있다.
public class PostRepository {
private Map<CacheKey, Post> cache;
public PostRepository() {
this.cache = new HashMap<>();
}
public Post getPostById(Integer id) {
CacheKey key = new CacheKey(id);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
Post post = new Post(); // DB에서 읽어오거나 REST API를 통해 읽어올 수 있다.
cache.put(key, post);
return post;
}
}
}
위 getPostById
메서드는 캐싱되어 있는 post 객체는 곧바로 반환해주고 그렇지 않은 post 객체는 cache
맵에 저장한 후 반환하는 메서드이다.
이런 구조 또한 주기적으로나 명시적으로 자원을 제거해 주지 않으면 cache
맵 내부에서 인스턴스를 참조하고 있기 때문에 가비지 컬렉터가 자원을 회수하지 않는다.
해결 방법으로는 cache
객체를 WeakHashMap
으로 바꿔주면 된다.
public class PostRepository {
private Map<CacheKey, Post> cache;
public PostRepository() {
this.cache = new WeakHashMap<>(); // WeakHashMap 사용
}
public Post getPostById(Integer id) {
CacheKey key = new CacheKey(id);
if (cache.containsKey(key)) {
return cache.get(key);
} else {
Post post = new Post(); // DB에서 읽어오거나 REST API를 통해 읽어올 수 있다.
cache.put(key, post);
return post;
}
}
}
WeakHashMap
을 사용하면 key 객체가 다른 곳에서 참조되어 있지 않으면 가비지 컬렉터가 해당 엔트리를 회수해 간다.
[참고]
백기선, inflearn 강의 이펙티브 자바 완벽 공략 1부
Joshua Bloch, 이펙티브 자바