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

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

  • 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
35
36
37
38
39
40
41
42
package effectivejava.chapter2.item7;
import java.util.*;

// 코드 7-1 메모리 누수가 일어나는 위치는 어디인가? (36쪽)
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);
    }

    public static void main(String[] args) {
        Stack stack = new Stack();
        for (String arg : args)
            stack.push(arg);

        while (true)
            System.err.println(stack.pop());
    }
}
  • 정상적인 클래스처럼 보이지만 위 예시는 메모리 누수 라는 치명적인 문제점이 존재한다.
  • 왜 그럴까? 바로 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수 하지 않기 때문이다.
    • 왜 가비지 컬렉터가 회수하지 않을까? 모르기 때문이다. elements 배열의 활성 영역 밖의 참조들이 모두 여기에 해당한다. 즉 size보다 높은 인덱스의 원소들은 GC가 참조되지 않고 있다는 것을 알 수가 없다.

null처리를 통해 쓰지 않는 메모리를 제거하라

  • 해법은 간단하다. 해당 참조를 다 썻을 때 null처리(참조 해제)하면 된다.
  • 이때는 위의 스택 예시에서 pop을 호출할 때이다.
1
2
3
4
5
6
7
8
//    // 코드 7-2 제대로 구현한 pop 메서드 (37쪽)
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }
  • 이렇게 다쓴 참조를 null처리하면 null처리한 참조를 실수로 사용하려 할 때 NPE를 던지며 종료된다.
    • 근데 이는? 런타임에 알게 되지 않을까? 이게 정말 장점일까? 라는 생각은 든다…..

그렇다고 모든 객체를 다 쓰자마자 일일이 null처리를 해야할까?

  • 그럴 필요도 없고, 바람직하지도 않다. 프로그램을 지저분하게 만든다.
  • 위와 같은 null처리는 예외적인 경우에만 하면 된다.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이라 하는데… 이는 위에서 말한 문제점이 이어질 것 같은데? 라는 생각이 든다.

그렇다면 null처리는 언제 해야할까?

  • 바로 스택처럼 자기 메모리를 직접 관리할 경우 메모리 누수에 취약할 경우이다.
  • 위에서 설명한 것처럼 비활성 영역에 elements 배열 요소들을 GC가 참조안하고 있단 것을 알 수가 없을때이다.

캐시 역시 메모리 누수를 일으키는 주범이다.

  • 객체 참조를 캐시에 넣고 나서, 이 사실을 까맣게 잊은 채 그 객체를 다 쓴뒤로도 한참을 그냥 놔두는 일이다.
  • 해법은 여러 가지다.
    • 운 좋게 캐시 외부에서 키(key)를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap 을 사용해 캐시를 만들자. 그리하면 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.
    • 단, WeakHashMap 은 이러한 상황에서만 유용하다는 사실을 기억하자.

Note: WeakHashMap과 관련해서는 여기를 참고하자.

  • 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 쓰지 않는 엔트리를 이따금 청소해줘야한다. 방법은 두 가지가 있을 것이다.
    • 1)ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용
    • 2)캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법(LinkedHashMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리)
  • 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 할 것이다.

리스너(listener) 혹은 콜백(callback) 역시 메모리 누수를 일으키는 주범이다.

  • 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다.
  • 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다.

정리: 메모리 누수는 겉으로 잘 드러나지 않기에 시스템에 수년간 잠복하는 사례도 있다. 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야만 발견되기도 한다. 그래서 이런 종류의 문제는 예방법을 익혀두는 것이 매우 중요하다.

This post is licensed under CC BY 4.0 by the author.

[Java] WeakHashMap

[Java] unmodifiableList