2장. 객체 생성과 파괴

GOAL
1. 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하기
2. 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법
3. 제때 파괴됨을 보장하고, 파괴 전에 수행해야 할 정리 작업을 관리하기

아이템7. 다 쓴 객체 참조를 해제하라

자바는 가비지 컬렉터를 갖췄으므로, 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아니다.

아래 코드에서는 숨어있는 문제가 있다. 메모리 누수이다.
이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 기비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다.
드문 경우로, 심할 때는 디스크 페이징이나 OufofMemoryError를 일으켜 프로그램이 예기치 않게 종료되기도 한다.

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);
    }
}

이 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체를 가비지 컬렉터가 회수하지 않는다.
이 스택이 그 객체들의 다쓴 참조를 여전히 가지고 있기 때문이다. (배열은 값을 넣기만 하고, 빼지는 않음)

해결법으로, 스택에서 꺼낼 때 그 위치에 있는 객체를 꺼내주고 그 자리를 null 처리를 하면 된다.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    this.elements[size] = null; // 다 쓴 참조 해제
    return result;
}

null 처리에 대하여

그렇다고 필요없는 객체를 볼 때마다 일일이 null 처리하는데 집중하지말자.
객체 참조를 null 처리하는 일은 예외적인 경우여야한다.

# 이럴 필요가 전혀 없음
Object age = 37;
...
...

age = null; 

다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.

 

메모리 누수 주범

1. 자기 메모리 직접 관리
자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다. 원소를 다 사용한 즉시 그 원소가 참조한 객체를 다 null 처리해줘야 한다.
 

2. 캐시
캐시 또한 메모리 누수를 일으키는 주범이다. 객체의 레퍼런스를 캐시에 넣어두고, 캐시를 비우는 것을 잊기 쉽다.
여러 해결방법이 있지만, 그 중에 WeakHashMap 해결책이 있다. 이는, 레퍼런스가 캐시 밖에서 필요 없어지면 해당 엔트리를 캐시에서 자동으로 비워준다.

 
WeakHashMap은 key 값을 모두 Weak 레퍼런스로 감싼다. 즉, WeakHashMap을 사용할 때 key 레퍼런스가 쓸모 없어졌다면, (key - value) 엔트리를 GC의 대상이 되도록해 캐시에서 자동으로 비워주는 것이다.

Object key1 = new Object();
Object value1 = new Object();

Map<Object, Object> cache = new HashMap<>();
cache.put(key1, value1);

Map<Object, Object> cache = new WeakHashMap<>(); // 이런게 있다. 

또는 특정 시간이 지나면 캐시값이 의미가 없어지는 경우에 대해 백그라운드 쓰레드( ScheduledThreadPoolExecutor) 를 사용하거나,
새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다. (LinkedHashMap - removeEldestEntry)
 
3. 리스너 혹은 콜백 (listener or callback)
클라이언트가 콜백을 등록만 하고, 명확히 해지하지 않으면, 콜백은 계속 쌓여갈 것이다.
이 때 콜백을 약한 참조(weak reference)로 저장하면, 가비지 컬렉터가 즉시 수거해간다.

결론

메모리 누수는 겉으로 잘 들어나지 않아 시스템에 수년간 잠복하는 사례도 있다.
철저한 코드리뷰, 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견이 되기도 한다.
예방법을 익혀두자!!!

참고

자료는 이펙티브 자바 책과 백기선님의 강의를 들으며 작성했다.
[이펙티브자바 - 아이템7] 다 쓴 객체 참조를 해제하라