개발 공부 기록하기/01. JAVA & Kotlin

짧은 코멘트와 함께하는 이펙티브 자바) #7 다 쓴 객체 참조를 해제하라

lannstark 2020. 9. 23. 05:42

짧은 코멘트

  1. 다 쓴 객체의 참조를 해제하지 않아 메모리 문제가 된적은 아직까지 경험해보지 못했다.
  2. 하지만 OOM이 발생한 적은 몇 번 존재했는데, OOM 발생시 heap dump를 뜨게 해둔 옵션이 OOM을 해결하는데 큰 도움을 주었다.
  3. JAVA 개발자라면 head dump 를 연습삼아 한 번 씩 떠보는 것도 좋아 보인다.

다 쓴 객체의 참조를 해제하라

다음 Stack 코드에서 이상한 부분을 발견할 수 있는가?

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

}

문제는 18번째 줄이다 이 Stack을 사용하게 되면, 점차 GC활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료되기도 할 것이다

해법은, 해당 참조를 다 썼을 때 null처리 (참조 해제) 하는 것이다

public Object pop() {
  if (size == 0)
    throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null;
  return result;
}

단순히 return elements[--size]를 하게 되면 거기에 담겨있던 element가 반환되어 사용되다 사용이 끝나더라도 GC는 이 element가 더 이상 사용되지 않는 element라는 것을 알 수 없다. 왜냐하면 Stack에서 elements[]의 내부 원소로 여전히 가지고 있기 때문이다.

객체 참조를 null로 처리하는 일은 예외적인 경우이다. 보통, 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다

자기 메모리를 직접 관리하는 클래스(컬렉션)라면 프로그래머는 항시 메모리 누수에 주의 해야 한다

캐시 역시 메모리 누수를 일으키는 주범이다. 객체의 ref를 캐싱해놓고 사용하다가 다 썼을 때 캐싱에서 해제하지 않으면 메모리 누수를 일으킬 것이다. 만약 캐시 외부에서 key를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하다면 WeakHashMap을 사용할 수 있다. 캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해주어야 한다 Scheduled ThreadPoolExecutor같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있다

메모리 누수의 또 다른 주범은 리스너 혹은 콜백이다. 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다. 이럴 때 콜백을 약한 참조로 저장하면 GC가 즉시 수거해 간다