본문 바로가기
이펙티브 자바

[아이템 7] 다 쓴 객체 참조를 해제하라

by 에드박 2021. 4. 24.

C, C++같은 언어를 써본적은 없지만 해당 언어에는 가비지 컬렉터가 없어서 직접 메모리를 관리해야 한다고 들었습니다.

자바처럼 가비지 컬렉터를 갖춘 언어는 프로그래머의 삶을 편안하게 만들어줍니다.

다 쓴 객체의 메모리를 알아서 회수해가기 때문입니다.

 

이때문에 자칫 메모리 관리에서 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 이것은 사실이 아닙니다.

 

package item7_20210424;

import java.util.Arrays;
import java.util.EmptyStackException;

public class MyStack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public MyStack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

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

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

 

위는 Stack을 간단히 구현한 클래스입니다.

이 클래스는 맡은 역할을 잘 수행하지만 한가지 숨은 문제점이 있습니다.

바로 '메모리 누수'입니다.

 

스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하 될것입니다.

심하면 디스크 페이징 또는 OutOfMemoryError를 일으켜 프로그램이 예기치않게 종료되기도 합니다.

 

그렇다면 위의 코드에서 메모리 누수는 어디서 일어날까요?

답은 스택이 커졌다가 pop으로 인해서 줄어들때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수해 주지 않아서 입니다.

 

 

size 는 4번째 자원을 자리키고 있고 o5 와 o6 는 실제로는 pop 이 된 상태로 사용하지 않는 자원이라 할 수 있습니다.

하지만 pop이 된 자원은 현재 가비지 컬렉터가 회수해주지 않고 있습니다.

pop했을 때 size(현재 자원을 가리키는 커서)를 1 줄이면 자원을 가리키는 위치만 변경 됐을뿐 pop으로 꺼낸 자원은 여전히 남아있는 상태입니다.

 

즉, elements 배열의 '활성 영역' 밖의 참조들이 모두 여기에 해당합니다. 활성 영역이란 size의 영역을 의미합니다.

 


가비지 컬렉션 언어에서 직접 자원을 해제하는 법

 

가비지 컬렉션 언어에서 의도치않게 객체를 살려두는 메모리 누수를 찾기가 아주 까다롭습니다.

객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못합니다.

그래서 단 몇개의 객체로 인해서 매우 많은 객체를 회수하지 못하게 될 수 있고 이는 잠재적으로 성능에 영향을 줄 수 있습니다.

 

간단한 해결방법으로 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 됩니다.

예시에서는 pop했을때 꺼낸 자원의 위치를 null로 초기화 해주면 됩니다.

 

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

 

 

이렇게 null로 처리했을 때 다른 이점도 있습니다.

만약 null로 처리한 자원을 사용하려고 하면 프로그램은 즉시 NullPointerException을 던지며 종료시킵니다.

 

주의해야 할점은 그렇다고 모든 자원을 사용하고 난 뒤에 null로 초기화해줄 필요는 없다는 것입니다.

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

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

 

현재 MyStack 클래스에서 메모리 누수가 생기는 원인은 자기 메모리를 직접 관리하기 때문입니다.

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

 


메모리 누수의 주범 중 하나 캐시

객체 참조를 캐시에 넣어두고 이 사실을 까맣게 잊은 채 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다.

 

이에 대한 해법은 여러가지가 있습니다.

  • 외부에서 키(key)를 참조하는 동안만(값이 아님) 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해서 캐시를 생성 -> 다 쓴 엔트리는 즉시 자동으로 제거됨
WeakHashMap 이란?

WwakHashMap 보다 Weak Reference 클래스에 대해 먼저 정리해보겠습니다.
Weak Reference(약한 참조) : null이 되면 GC 대상이 된다, 메모리가 부족하지 않더라도 GC의 대상이 될 수 있습니다.

WeakHashMap 은 Key에 해당하는 객체가 더이상 활용되지 않는다고 판단하면 해당 객체를 제거합니다.

 

  • 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식 -> 쓰지않는 엔트리를 이따금씩 청소 해줍니다.
  • 백스라운드 스레드를 활용하는 방법
    • ScheduledThreadPoolExecutor
  • 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법
    • LinkedHashMap - removeEldestEntry 메서드 활용

 


메모리 누수의 주범 리스너(Listener) 혹은 콜백(callback)

클라이언트가 콜백을 등록만 하고 해지하지 않는다면, 뭔가 조치해주지 않는한 콜백은 계속 쌓여갈 것입니다.

 

클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치를 해주지 않는 한 콜백은 계속 쌓여갈 것입니다.

이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해갑니다.

 

예를들어 WeakHashMap에 키로 저장하면 됩니다.

댓글