[이펙티브 자바] 아이템7 | 다 쓴 객체 참조를 해제하라

제롬·2022년 1월 7일
0

이펙티브자바

목록 보기
7/25

메모리 누수를 주의해야 한다.

자바에서는 가비지 컬렉터가 다쓴 객체를 알아서 회수해간다 하지만 그렇다고해서 메모리 관리에 신경쓰지 않으면 안된다. 메모리 누수가 발생하는 프로그램을 오래 실행하다보면 점차 가비지 컬렉션 활동메모리 사용량이 늘어나 결국 성능이 저하되거나 메모리초과(OufOfMemory) 오류가 발생할 수 있다.

[메모리 누수가 일어나는 스택 구현]

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

        return elements[size--];
    }

    private void ensureCapacity(final Object e) {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

위 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.

그 이유는 스택이 그 객체들의 다 쓴 참조(obsolete reference) 를 여전히 가지고 있기 때문이다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 따라서 잠재적으로 성능에 악영향을 줄 가능성이 있다.

앞의 코드에서는 elements 배열의 '활성 영역' 밖의 참조들이 모두 여기에 해당된다.
('활성 영역' 은 인덱스가 size보다 작은 원소들로 구성된다.)

메모리 누수 방지 방법

메모리 누수를 방지하는 방법은 간단하다. 해당 참조를 다 사용했을 때 null 처리(참조 해제)하면 된다.

[메모리 누수를 방지하는 스택 pop메서드]

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object result = elements[--size];
        elements[size] = null;

        return result;
    }

다 쓴 참조를 null 처리하면 따라오는 부수적인 이점이 있다. 만약 null 처리한 참조를 사용하려하면 NullpointerException 이 발생하며 종료된다.

모든 객체를 null처리 해야할까?

사실 모든 객체를 다 쓰자마자 null 처리하는 것은 별로 바람직하지 않다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

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

참조를 변수 유효범위 밖으로 밀어내는 일은 변수의 범위를 최소로 정의했다면 자연스럽게 이루어진다.

그렇다면 null처리는 언제해야 할까?

Stack클래스가 메모리 누수에 취약한 이유는 자기 메모리를 직접 관리하기 때문이다.

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

위에서 예를 들었던 스택은 객체 자체가 아니라 객체 참조를 담는 elements 배열로 메모리를 관리한다. 문제는 배열의 비활성 영역은 쓰이지 않는다는 것이고 가비지 컬렉터는 이 사실을 알지 못한다.

그러므로 프로그래머는 비활성 영역이 되는 순간 null 처리를 통해 해당 객체가 더 이상 쓰이지 않는다는 사실을 가비지 컬렉터에게 알려주어야 한다.

추가적으로 주의해야 할 메모리 누수 사례

캐시 메모리 누수

객체 참조를 캐시에 넣어두고 이를 방치하는 일이 자주 발생한다. 이를 해결하는 방법에는 여러가지가 있다.

  • WeakHashMap 을 사용해 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시를 사용하는 방식.
  • ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용해 엔트리를 청소하는 방식.
  • LinkedHashMapremoveEldesEntry 메서드를 활용해 엔트리를 청소하는 방식.
  • java.lang.ref 패키지를 이용해 더 복잡한 캐시를 만드는 방법.

리스터 혹은 콜백

클라이언트가 콜백을 등록만하고 명확히 해지하지 않는다면, 콜백은 계속 쌓이게 된다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터 가 즉시 수거해 간다.
ex) WeakHashMap에 키로 저장하면 된다.

WeakHashMap

일반적인 HashMap의 경우 KeyValue가 put되면 사용여부에 관계없이 해당 내용은 제거되지 않는다. 하지만 weakHashMap의 경우 Key에 해당하는 어떤 객체가 null이 되면 해당 객체를 key로 하는 HashMapElement도 더이상 사용하지 않는다고 판단되어 자동으로 제거되어 버린다.

[WeakHashMap]

public class WeakHashMapTest {
    public static void main(String[] args) {
        final WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>();

        Integer key1 = 1000;
        Integer key2 = 2000;
        Integer key3 = 3000;

        weakHashMap.put(key1, "test1");
        weakHashMap.put(key2, "test2");
        weakHashMap.put(key3, "test3");

        weakHashMap.entrySet().forEach(System.out::println);

        weakHashMap.put(1000, null); 
        System.gc(); // gc 실행 후 결과
        weakHashMap.entrySet().forEach(System.out::println);

        key1 = null;
        weakHashMap.entrySet().forEach(System.out::println);

        System.gc(); 
        weakHashMap.entrySet().forEach(System.out::println);
    }
}
// 실행결과
3000=test3
2000=test2
1000=test1
3000=test3
2000=test2
1000=null
3000=test3
2000=test2
1000=null
3000=test3
2000=test2

정리

메모리 누수는 겉으로 잘 드러나지 않기 때문에 이런 종류의 문제는 예방법을 익혀두는것이 중요하다. 그렇지 않으면 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료될 수 있다.

이렇게 메모리누수를 방지하는 방법으로 다 쓴 참조 객체를 null처리 해주는 방법이 있다. 하지만, 사실 모든 객체를 다 사용 후 null처리하는것은 별로 바람직하지 않다.

다 사용한 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수의 범위를 최소한으로 지정하여 사용 후 유효범위 밖으로 자연스레 밀려나게 하는 것이다.

단, 자기 메모리를 직접 관리하는 클래스의 경우 참조한 객체를 사용 후 null 처리해주어야 한다.
예를들어, 위에서 본 스택의 경우 스택에서 꺼내진 객체들에 대한 다 쓴 참조를 여전히 스택이 가지고 있다. 꺼내진 객체들은 비활성영역에 해당하는 객체들인데 가비지 컬렉터는 이런 비활성영역에 대한 객체들을 회수하지 못한다. 따라서 이런 경우 null처리를 통해서 객체가 사용하지 않는다는 사실을 가비지 컬렉터에게 알리고 메모리 누수를 방지하자.

0개의 댓글