나를 기록하다
article thumbnail
반응형

이펙티브 자바(출처: yes24)

가비지 컬렉터가 있더라도 메모리 관리를 해야 한다

[예시] 스택을 간단히 구현한 코드

메모리 누수가 일어나는 위치는 어디인가?

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

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

메모리 누수는 어디서 일어나는가?

  • 이 코드에서 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
    (프로그램에서 그 객체들을 더 이상 사용하지 않더라도)
  • 이유: 이 스택이 그 객체들의 다 쓴 참조(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 처리하는 일은 예외적인 경우여야 한다.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것
  • 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이뤄진다.

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

  • Stack 클래스가 메모리 누수에 취약한 이유는 스택이 자기 메모리를 직접 관리하기 때문이다.
위 예시의 스택은 객체 자체가 아니라 객체 참조를 담는 elements 배열로 저장소 풀을 만들어 원소들을 관리한다.
배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다.
하지만 가비지 컬렉터는 이 사실을 알 수 없고 가비지 컬렉터가 보기에는 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체다.
그러므로 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.
  • 캐시 또한 메모리 누수를 일으키는 주범
  • 객체 참조를 캐시에 넣고 그대로 두는 경우가 많다.
    • 해법 1. 운 좋게 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들어라. 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다. 단 WeakHashMap은 이러한 상황에서만 유용하다.
    • 해법 2. 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어려워 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서 쓰지 않는 엔트리를 청소해줘야 하는데 (Scheduled ThreadPoolExecutor 같은) 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다. LinkedHashMapremove EldestEntry 메서드를 사용하여 후자의 방식으로 처리한다. 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 활용해야 한다.
    • 해법 3. 리스너(listener) 혹은 콜백(callback). 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 조치가 없는 한 콜백은 계속 쌓여간다. 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다. WeakHashMap에 키로 저장하면 된다.
반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...