본문 바로가기

책리뷰/이펙티브자바

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

728x90
반응형

자바에서는 다쓴 객체를 알아서 회수해가는 가비지 컬렉터가 있다. 

자칫 메모리 관리에 신경을 쓰지 않아도 된다고 오해할 수 있는데 절대 아니다.

 

Stack클래스의 메모리 누수


public class Stack {
    private Object[] elements;
    private int size = 0;
    // 처음 stack 사이즈
    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];
    }

    // 배열이 다 찼을 경우 배열을 복사한 후 2배가 되는 배열을 만든다. 
    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

}

스택이 커졌다가 줄어들 때 스택에서 꺼낸 객체들은 프로그램에서 더 이상 그 객체를 사용하지 않더라도 GC가 회수하지 않는다. GC는 프로그램상에서 사용하지 않는 객체를 판단할 수 없다. 객체 참조를 여전히 갖고 있기 때문에 이런 경우 메모리 누수가 발생할 수 있다.

객체 참조 하나를 살려두면 GC는 그 객체가 참조하는 모든객체를 회수해가지 못한다. 

 

이런 객체 참조를 해제하는 방법을 다 썼을 때 null처리를 하면 된다. 

 

객체 참조 해제 방법


public Object pop() {
    if(size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

객체의 참조가 더이상 필요없어지는 시점은 스택에서 꺼낼 때 이므로 꺼내면서 참조를 해제하면 된다. 

 

그렇다고 모든 객체를 쓰자마자 null처리를 할 필요는 없다.

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

 

 

Null처리는 언제 해야할까?


Stack클래스가 메모리 누수에 취약한 이유는 자기 메모리를 직접 관리하기 때문이다. 

elements 배열로 원소를 관리하는데 사용되고 비활성된 영역은 쓰이지 않는다.

GC가 보기엔 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체이기 때문에 필요없다는 사실은 프로그래머만 알 수 있다. 

 

자기 메모리를 직접관리하는 클래스일 경우 메모리 누수에 주의해야한다. 

 

메모리 누수에 주의해야하는 경우


캐시 : 객체 참조를 캐시에 넣고 나서 객체를 다쓴 뒤로도 한참을 놔두는 경우가 있을 수 있다. 

👉 엔트리가 살아있는 캐시가 필요한 경우 WeakHashMap을 사용해 캐시를 만들자.

다쓴 엔트리는 자동으로 제거된다.(WeakHashMap은 이런 경우에만 유용하다.)

 

리스너 혹은 콜백 : 클라이언트가 콜백을 등록하고 명확히 해지 않을 경우 콜백은 계속 쌓여간다. 

👉 콜백을 약한 참조로 저장하면 GC가 수거해간다.(ex, WeakHashMap)

 

WeakHashMap


 

일반적인 HashMap과 동일하지만 key가 weak reference된 형태를 말한다.

키가 참조하고 있는 값이 메모리에서 해제되면 WeakHashMap의 엔트리도 제거된다.

Map<Integer, String > map = new WeakHashMap<>();

Integer key1 = 128;
Integer key2 = 129;

map.put(key1, "key1");
map.put(key2, "key2");

key1 = null;

System.gc();

map.entrySet().forEach(System.out::println);


// 결과 
256=key2

String은 JVM에 의해 다른 곳에 저장되기 때문에 Strong reference로 남는다.

그렇기 때문에 String을 key로 사용한다면 WeakHashMap을 쓸 이유가 없다.

JVM에 따라 다르지만 Integer.valueOf(10)형태로 쓰이는 경우나 Autoboxing으로 사용하는 경우 String과 같이 Cache된 value를 사용하기 때문에 WeakHashMap을 쓰면 문제가 된다. 

(cache하는 max값은 128 혹은 256)

728x90
반응형