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

최강일·2024년 4월 6일
0

Effective Java

목록 보기
6/9

c,c++처럼 메모리를 직접 관리하는 언어를 사용하다 java를 사용하면 가비지컬렉터로인해 직접 메모리를 해제하지 않아도된다.
하지만 이는 메모리 관리에 더 이상 신경쓰지 않아도 된다는 말이 아니다.

메모리 누수를 일으키는 주범들이 있다.

주범1. 메모리를 직접 관리하는 클래스

메모리 누수가 일어나는 위치는?

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

메모리 누수 발생 포인트

Stack에서 pop을 하게 되면, element 상의 포인터는 한칸 내려온다.
하지만 그 이전에 element에서 참조하고 있는 객체는 그대로 있다.
포인터만 한칸이 내려온 것이다.
element에서 참조하고 있는 그 객체는 가비지컬렉터가 회수하지 못한다.
결국 메모리 누수가 계속 발생한다.

즉, Stack에서 pop되어 더이상 사용되지 않는 객체라고는 하지만, 그것은 우리나 아는 얘기다.
java 입장에서는 참조되고 있는 객체이기 때문에 사용되고 있다고 생각한다.
그렇기 때문에 가비지컬렉터는 객체를 회수하지 못하게 된다.

제대로 구현한 pop 메서드

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

객체를 pop 하고 나서 null 처리를 통해 참조를 해제해주면 된다.

이렇게 메모리 누수로 인한 프로그램의 성능 저하를 느껴본 개발자들은,
null 처리를 통해 다 쓴 객체 참조를 해제하는데 집중하기도 한다.
하지만 그럴 필요도 없고, 오히려 프로그램을 지저분하게 만들기 때문에 지양된다.

그렇다면 언제 null 처리가 필요할까?

일반적으로 Stack 처럼 자기 메모리를 직접 관리하는 클래스의 경우, 프로그래머는 메모리 누수를 신경써야 한다.
클래스가 스스로 메모리를 관리한다는 것은, 가비지컬렉터가 관여할 수가 없다는 것이다.
그렇기에 앞선 Stack의 예처럼 객체를 다 쓰고 나면, null 처리를 통해 해당 객체는 더이상 쓰이지 않는다는 것을 알려줘야 한다.

주범2. 캐시

캐시 또한 메모리 누수를 일으키는 주범이다.
객체 참조를 캐시에 넣고 까먹어서 계속 자리를 차지하고 있는 것이다.

해결방법은 다양하다.

WeakHashMap 사용하기

만약 캐시 외부에서 키(Key)를 참조하는 동안만 객체가 살아 있도록 하고 싶다면, WeakMapHash를 사용하는 것이 좋다.

이렇게 하면, 다 쓴 객체는 자동으로 캐시에서 제거된다.
다만, 이는 위와 같이 특정한 상황에서만 유용하다.

LinkedHashMap 사용하기

보통 캐시를 만들 때, 캐시 객체의 유효 기간을 정확히 파악하는게 어렵다.
그래서 시간이 지날수록 객체의 가치를 떨어뜨리는 방식을 흔히 사용한다.

이럴 경우, 쓰지 않는 객체를 한번씩 청소해주는 작업이 필요하게 된다.
백그라운드 스레드를 활용할 수도 있지만, 캐시에 새 객체를 추가할 때 부수 작업을 추가하는 방식도 있다.
LinkedHashMap의 경우, removeEldesEntry 메서드를 통해 객체를 정리해준다.

주범3. 리스너 혹은 콜백

리스너 (Listener) 혹은 콜백 (Callback)이라고 불리는 것은, 클라이언트가 등록 및 사용하고 난 후에 명확히 해지하지 않는다면 계속해서 쌓여간다.
이럴 경우, 콜백을 약한 참조 (weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해갈 수 있다.
그 대표적인 예시가 바로 앞서 한번 말했던 WeakHashMap 이다.

정리

메모리 누수는 겉으로 잘 드러나지 않기에 이를 프로파일링할 여러가지 툴들을 활용하곤 한다.
그만큼 난이도가 높은 문제이기에 예방법을 익혀두는 것도 중요하다.

profile
Search & Backend Engineer

0개의 댓글