JAVA/Effective Java

item7. 다 쓴 객체 참조를 해제하라

Garonguri 2022. 4. 16. 16:32
728x90

Java의 특징 : GC! GC가 있어 다 쓴 객체를 알아서 회수해간다.

- 따라서 메모리 관리를 개발자가 신경쓰지 않아도 될까? -> NO!

 

-> 자칫하다간  메모리 누수  문제가 일어날 수 있다. 메모리를 직접 관리하는 클래스는 개발자가 메모리 누수에도 신경써야 한다.

 

 

[메모리 문제가 일어날 수 있는 경우]

1. Stack 사용 시

스택은 자기 메모리를 직접 관리한다. 배열로 저장소 풀(pool^.^)을 만들어 원소들을 관리한다. 

가용 영역은 사용되며, 유효 영역 밖의 원소는 사용하지 않는데, GC는 이러한 정보를 해석할 수 없다.

// Can you spot the "memory leak"?
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }
    /*-------------- Bad ------------------*/
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        return this.elements[--size];
    }

    /*-------------- Good ------------------*/
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }

    /***
     * 원소를 위한 공간을 적어도 하나 이상 확보한다.
     * 배열 크기를 늘려야 할 때마다 배열의 크기가 대략 2배씩 늘어난다.
     */
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }

}

- Bad code

  • 스택에 원소를 계속 쌓다가 원소를 pop했을지라도, 스택이 차지하고 있는 메모리는 줄어들지 않는다.
  • 스택의 구현체는 필요 없는 객체에 대한 참조를 그대로 가지고 있다.
  • pop()메소드의 this.element[--size]와 같은 경우, 실제 값은 삭제되지 않는다. 이런 경우 메모리 누수가 발생할 수 있다.
  • 즉 size보다 작은 부분의 범위 내에서 사용가능하지만, size보다 큰 부분(활성 영역 밖)에 있는 값들은 사용 불가하며 메모리만 차지하고 있다.
  • 따라서, 비활성 영역의 객체가 더이상 사용하지 않는다는 것을 GC에게 알려주기 위해 null처리를 해서 GC에게 알려야 한다.

왜 이런 경우가 생기는 걸까? -> 다 쓴 객체 참조를 여전히 가지고 있기 때문이다.

어떻게 해결할 수 있을까? -> 다 쓴 객체의 참조를 해제하면 된다.

 

- Good code

  • 스택에서 꺼낼 때, 해당 위치에 있는 객체를 null로 초기화해준다. 따라서 GC가 참조를 해제한다.
  • 메모리 누수도 막을 수 있으며, 해당 요소가 null로 초기화한 공간을 사용하려 하면 nullpointException을 발생시켜 주기 때문에, 프로그램 오류를 조기에 발견할 수 있다.

그렇다고 필요 없는 객체가 생길때마다 null처리를 하면 안된다.

객체를 null로 초기화 하는 것은 매우 예외적인 경우로, 과용하면 프로그램을 지저분하게 만들 수 있다.

 

필요없는 객체를 정리하는 최선의 방법은

참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.

(로컬 변수는 유효범위 밖으로 넘어가면 GC가 정리해주므로)

item 57에도 나온대요

 

2. 캐시

객체 참조를 캐시에 넣고. 객체를 다 쓴 뒤에도 캐시에 그대로 두는 경우는 메모리 누수를 발생시킬 수 있다.

[해결 방법]

1. WeakHashMap을 사용해 캐시를 만든다.

  • 한계 : 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 경우에만 유용한 방법이다.

2. 캐시 엔트리 유효 기간을 정한다.

  • 시간이 지날 수록 엔트리의 가치를 떨어트리는 방식을 흔히 사용한다.
  • 한계 : 캐시 엔트리 유효 기간은 정확히 정의하기 어렵다.
  • 이런 경우, 쓰지 않는 메모리를 청소해야 한다.
    • 백그라운드 스레드(Scheduled ThredPoolExecutor)를 활용하거나, 캐시에 새 엔트리를 추가할 때 부가적으로 기존 캐시를 비우는 일을 수행한다.
    • LinkedHashMap의 경우 removeEldestEntry 메서드를 사용한다.

3. 리스너, 콜백

  • Callback이란? : 이벤트가 발생하면 특정 메소드를 호출해 알려주는 것.
  • Listner이란? : 이벤트가 발생하면 연결된 리스너들에게 이벤트를 전달한다.
  • 클라이언트 코드가 콜백을 등록만 하고 해지하는 방법을 제공하지 않는다면, 콜백이 쌓여 메모리 누수가 일어날 수 있다.
    • 이런 경우 콜백을 Weak reference로 저장해 GC가 수거해갈 수 있게 한다.
    • ex) WeakHashMap에 키로 저장한다.
728x90