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

June·2022년 3월 23일
0

[이펙티브자바]

목록 보기
7/72

자바는 가비지 컬렉터가 있지만 메모리 관리에 신경을 안써도 되는 것은 아니다.

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

}

위 코드는 메모리 누수 문제가 있다.

스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 갖고 있기 때문이다.
elements 배열의 활성 영역(인덱스가 size 보다 작은 곳) 밖의 참조들을 가리킨다.

가비지 컬렉션 언어에서는 메모리 누수를 찾기 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체를 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체..)를 회수해가지 못한다.

여기서는 Stack 자체가 정리되지 못한다. Stack이 갖고 있는 Object[] elements가 가지고 있는 요소들이 정리되지 못하기 때문이다.

해법은 간단하다. 해당 참조를 다 썼을 때 null 처리 (참조 해제)하면 된다.

public class Stack {
	...

    // 코드 7-2 제대로 구현한 pop 메서드 (37쪽)
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }
}

다 쓴 참조를 null 처리하면 다른 이점도 따라오는데, 다른 참조를 사용하려하면 NullPointerException을 발생시킬 것이다.

하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 이 변수의 범위를 최소가 되게 정의(아이템 57)했으면 자연스럽게 이뤄진다.

메모리 직접 관리

Stack 클래스가 문제가 됐던 이유는 메모리를 직접 관리했기 때문이다. elements 배열로 저장소 풀을 만들어 원소들을 관리했다. 배열의 활성 영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다. 가비지 컬렉터는 이걸 알 수 없다. 가비지 컬렉터 입장에서는 비활성 영역에서 참조하는 객체도 똑같이 유효하다. 그래서 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 알려야 한다.

자기 메모리를 직접 관리하는 클래스라면 프로그래머는 메모리 누수에 주의해야 한다.

캐시

캐시 역시 메모리 누수를 일으키는 주범이다. 객체 참조를 캐시에 넣고, 객체를 다 쓴 뒤에도 한참을 놔두는 일이 자주 있다.

Object key1 = new Object();
Object value1 = new Object();

Map<Object, List> cache = new HashMap<>();
cache.put(key1, value1);

key1이 없어지면 이 캐싱 자체가 무의미해지는 경우가 많다.

해법

  • 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 키시를 만들자. 다 쓴 엔트리는 자동으로 제거된다.
Object key1 = new Object();
Object value1 = new Object();

Map<Object, List> cache = new WeakHashMap<>();
cache.put(key1, value1);

이 키를 Weak라는 래패런스로 감싸서 들어간다. 하드 레퍼런스 (new 로 생성한 일반적인 방법)가 없어지면 정리된다.

  • 캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵다. 그래서 시간이 지날 수록 엔트리의 가치를 떨어뜨리는 방식을 쓰기도 한다. 여기서는 엔트리를 종종 청소해줘야 한다.

콜백

메모리 누수의 또 다른 원인은 리스너 혹은 콜백이다. 콜백 등록만 하고 해지 하지 않으면 콜백은 싸여만 간다.

Weak Reference

가바지 컬렉터의 대상이 되려면 그 객체를 가리키는 레퍼런스가 전부 없어져야 한다.
WeakReference의 경우 String Reference만 없어지면 WeakReference 자체가 가비지 컬렉터의 대상이될 수 있다.

WeakReference weakWidget = new WeakReference(widget);

이 예시에서 widget이 없어지면 WeakReference가 차지하는 메모리 자체가 없어질 수 있다.

참고

http://euler.mat.uson.mx/~havillam/java/Common/Understanding-Weak-References.pdf

0개의 댓글