Home [Effective Java] Item07. 다 쓴 객체 참조를 해제하라!
Post
Cancel

[Effective Java] Item07. 다 쓴 객체 참조를 해제하라!

Item07. 다 쓴 객체 참조를 해제하라!

  • C, C++ 처럼 메모리를 직접 관리해야 하는 언어와 비교해 자바는 가비지 컬렉터를 통한 편리함을 느낄 수 있다.
  • 그렇다면 자바 사용 시 메모리 관리에 소홀해도 되는가? 그건 아니다.

메모리 누수

메모리 누수의 첫번째 주범 : 스택

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 Stack(int size) {
        this.elements = new Object[size];
    }

    public void push(final Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

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

    /**
     * 원소를 위한 공간을 적어도 하나 이상 확보
     * 배열 크기를 늘려야 할 때마다 2배씩 늘림
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, (size * 2) + 1);
        }
    }
  • 위 코드는 특별한 문제가 없어 보이지만 숨은 문제가 있다. 바로 “**메모리 누수**“다.
  • 이 스택을 사용하는 프로그램을 오래 실행한다면 점차 가비지 컬렉터의 활동과 메모리 사용량이 증가하게 된다.
  • 심한 경우에는 디스크 페이징 또는 OOM 에러를 일으켜 종료된다.

페이징

프로그램 중 자주 사용되지 않는 부분의 작업 메모리를 주기억장지(메모리) -> 보조기억장치(HDD,SSD)로 옮기는 방식을 통해 메모리 관리하는 기법

메모리누수

가비지 컬렉터에 의해 메모리가 정리되지 않고, 프로그램이 계속해서 점유하고 있는 현상

메모리 누수는 어디서 일어날까?

  • 위 코드에서는 스택이 커졌다가 줄어들 때 꺼내진 객체를 가비지 컬렉터가 회수하지 않아 누수가 생긴다.
  • 사용하지도 않는 객체인데 왜 회수하지 않는가? 스택이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

  • 위 코드를 기준으로 elements 배열의 ‘활성 영역’ 밖 참조들이 다시는 사용하지 않을 다 쓴 참조에 해당한다.

가비지 컬렉터의 동작

  • 가비지 컬렉션 언어 -> 메모리 누수를 찾기가 아주 어렵다.
  • 왜? 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체 뿐만 아니라 그 객체가 참조하는 모든 객체를 연쇄적으로 살려두기 때문이다.
  • 이는 단 몇개의 객체가 수많은 객체의 회수를 막는 사태를 일으킬 수 있다.
  • 해법? 해당 참조를 다 썼을 때 null 처리해주는 것이다.

  • 위 스택 코드를 기준으로 보면 참조가 더 이상 필요 없게 되는 부분은 pop() 메서드가 동작할 때이다.
1
2
3
4
5
6
7
// 제대로 구현한 pop 메서드
public Object pop() {
  if (size == 0) throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null; // obsolete reference
  return result;
}

다 쓴 참조를 null 처리해줄 때의 득

  • null 처리한 참조를 실수로 사용하려고 할 때 NullPointerException을 던져 프로그램 오류를 조기에 해결할 수 있다.

다 쓴 참조를 null 처리해줄 때의 실

  • 프로그램이 필요 이상으로 지저분해질 수 있다.
  • 그렇기 때문에 객체 참조를 null 처리해주는 일은 예외적인 경우에 한해 진행하면 좋다.
  • 일반적으로는 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 방법을 사용하는 것이 좋다.

왜 Stack 클래스는 메모리 누수에 취약한가?

  • 스택이 자기 자신의 메모리를 직접 관리하기 때문이다.
  • 스텍은 객체 자체가 아니라 객체 참조를 담는 배열 풀을 만들어 원소를 관리한다.
  • 이 배열의 원소 중 활성 영역에 속한 원소만이 사용되지만 가비지 컬렉터는 이 사실을 알 수가 없다.
  • 그렇기에 프로그래머는 객체의 활용 여부를 가비지 컬렉터에 명시해줘야한다. 이 과정에서 null 처리가 필요하다.

메모리 누수의 두번째 주범 : 캐시

  • 객체 참조를 캐시에 넣고 나서, 그 사실을 잊고 한참을 놔두는 일이 빈번하다.
  • 이러한 경우 메모리 누수가 일어난다.

캐시로 인한 메모리 누수의 해결책

  • 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시를 사용하는 경우 -> WeakHashMap을 사용해 캐시를 만든다. 그럼 다 쓴 엔트리는 자동적으로 제거된다.
  • 엔트리의 유효기간을 설정한다. 주로 엔트리가 생성되고 시간이 지날수록 가치를 하락시키는 방식을 사용한다. 가치 하락이 많이 일어난 사용하지 않는 엔트리는 이따금 청소해줘야 한다.

메모리 누수의 세번째 주범 : 리스터 혹은 콜백 호출

  • 클라이언트가 콜백을 호출하고 명백히 해지하지 않는다면 콜백이 쌓여 누수가 발생한다.
  • 이러한 경우 콜백을 약한 참조(weak reference)로 저장해 가비지 컬렉터가 즉시 수거하도록 구성할 수 있다.

추가 정리

  • 대표적 사례 외에도 메모리 누수가 발생하는 사례들이 있다.
  • 이러한 경우는 코드 리뷰나 힙 프로파일러 같은 디버거 사용을 통해 발견하고 해결할 수 있도록 경험을 쌓아야 한다.
This post is licensed under younghwani by the author.

[Effective Java] Item06. 불필요한 객체 생성을 피라하!

[Effective Java] Item08. finalizer와 cleaner 사용을 피하라!

Comments powered by Disqus.