Effective Java #7 - Memory Leak(메모리 누수)

업데이트:


객체 생성과 파괴 (2장)

#7 : 다 쓴 객체 참조를 해제하라

메모리 리크가 생길 수 있는 포인트에 대해 알아보자.

개요

  • Java에는 GC(가비지 컬렉터)가 있어서 메모리 관리에 신경을 쓰지 않아도 된다고 생각할 수 있는데 그렇지않다. 아래 예시들을 통해 메모리 관리의 중요성을 알아보자.

#1 메모리를 직접 관리하는 클래스

  • Stack의 예로 문제를 볼 수 있다.

  • 스택의 사이즈가 커졌다가 줄어들 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.

  • 아래 예시의 스택의 경우 pop을 해도 스택에서 여전히 사용이 끝난 참조값(obsolete reference)을 갖고 있다.

  • 이를 비활성 영역이라고도 할 수 있으며 가비지 컬렉터는 비활성 영역이 사용하지 않는 레퍼런스라는 것을 알 방법이 없다.

  • 여기서 문제는 다 쓴 레퍼런스 값의 메모리 누수 외에도 해당 객체가 참조하고 있는 모든 객체(그리고 그 객체가 참조하는 모든 객체)를 가비지 컬렉터가 회수 할 수 없어서 추가적인 메모리 누수가 발생한다.

    public class Stack {
      
        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) {
            this.ensureCapacity();
            this.elements[size++] = e;
        }
      
        public Object pop() {
            if (size == 0) {
                throw new EmptyStackException();
            }
            return this.elements[--size]; /* memory leak point */
        }
      
        private void ensureCapacity() {
            if (this.elements.length == size) {
                this.elements = Arrays.copyOf(elements, 2 * size + 1);
            }
        }
    }
    
  • 이러한 문제를 해결하기 위해 사용이 끝난 객체에 null 처리(참조 해제) 하면 된다. (실제 Stack의 경우도 아래와 같이 null 처리를 해준다.)

    public Object pop() {
      if (size == 0) {
        throw new EmptyStackException();
      }
      Object value = this.elements[--size];
      this.elements[size] = null;
      return value;
    }
    
  • 객체를 null 처러히면 다음 GC 발생 시 자동으로 레퍼런스가 정리된다.

  • 그러나 모든 객체를 다 쓰자마자 null 처리하는 것은 바람직하지도 않고 프로그램을 지저분하게 만든다.

  • 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

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

#2 캐시

  • 캐시 역시 메모리 누수를 일으키는 주범이다. 객체의 레퍼런스를 캐시에 넣어 놓고 캐시를 비우는 것을 잊기 쉽다.
  • 빠른 속도를 위해 사용하는 캐시에 자원이 계속 쌓이게 되면 캐시는 본래 역할을 못하게 된다.
  • 해결방법1 : WeakHashMap을 사용 할 수 있다.
    • WeakHashMap 은 특정 key 값이 더이상 사용되지 않는다고 판단되면 해당 Key - Value 쌍을 삭제해준다.
    • 캐시의 키에 대한 레퍼런스가 캐시 밖에서 필요 없어지면 엔트리를 캐시에서 자동으로 비워주는 역할이다.
  • 해결방법2 : 새로운 엔트리를 추가할 때 부가적인 작업으로 기존 캐시를 비우는 일이다.
    • 캐시를 만들 때 보통 캐시 엔트리의 유효기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다.
    • LinkedHashMap - removeEldestEntry()

#3 리스너(listener) 혹은 콜백(callback)

  • 클라이언트가 콜백을 등록만 하고 해지하지 않는다면 콜백은 계속 쌓여만 갈 것이다.
  • 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다.
  • 해결방법으로 WeakHashMap을 사용할 수 있다.

핵심정리

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

스터디 저장소

References

댓글남기기