티스토리 뷰

728x90
반응형

다 쓴 객체 참조를 해제하라


  • 자바에서는 가비지 컬렉터가 메모리를 알아서 관리 해주니까 프로그래머가 메모리 관리에 신경써야 하는 부분이 적습니다. 그래서 자칫 메모리 관리에 더 이상 신경쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아닙니다.
  • 일반적으로 객체 참조가 해제되서 객체를 참조하는 곳이 없게되면 가비지 컬렉터는 해당 객체를 회수해갑니다.

 

 

💡예제 코드

  • 아래 예제 코드에서는 스택이 커졌다가 줄어들었을 때 스택에서 꺼낸 객체들을 가비지 컬렉터가 회수하지 않습니다. 프로그램에서
    해당 객체를 더 이상 사용하지 않더라도 회수를 하지 않습니다.
  • 예제 코드에서는 스택이 배열에 다 쓴 참조를 여전히 가지고 있습니다.(다 쓴 참조란 더이상 쓰이지 않을 참조란 의미)
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 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);
        }
    }
}

 

💡문제가 발생한 부분은?

  • 아래 pop() 메서드를 호출할 때마다 size가 줄어들면서 객체배열 elements에서 size 인덱스에 위치한 값들을 반환하고 있습니다. 
  • 예를들어 elements라는 객체 배열의 크키가 5이고 [1, 2, 3, 4, 5] 라는 값이 들어 있을 때 pop() 메서드를 2회 호출해서 현재 size는 3일 것입니다. 여기서 pop() 메서드로 꺼낸 객체 4, 5는 어딘가에서 두둥실 떠 다닐것입니다. 
  • 궁금증. 다시 push() 메서드를 사용해서 다른 값을 밀어 넣으면 기존 값들은 가비지 컬렉터가 회수하는게 아니였던건가?
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    return elements[--size];
}

 

💡해결 방안

  • 해당 참조를 다 썻을 때 null처리를 해줍니다.
  • 이렇게 다 쓴 참조를 null 처리하면 다시 사용하려 했을 때 NPE가 발생하기에 비정상적인 접근도 확인할 수 있습니다.
  • 하지만 객체 참조를 null 처리하는 것은 예외적인 경우여야하고 더 나은 방법은 다 쓴 참조를 유효 범위 밖으로 밀어버리는 것입니다.
  • 일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 합니다. 원소를 다 사용한 즉시 그
    원소가 참조한 객체들을 다 null 처리해줘야 합니다.
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

 

💡Java의 Stack 클래스

  • 자바의 스택은 자기 자신의 메모리를 직접 관리합니다. 그렇기 때문에 메모리 누수에 취약합니다.
  • Stack 클래스도 내부적으로는 가비지 컬렉터가 일을 제대로 할 수 있도록 null로 처리를 하는 것을 알 수 있습니다.
public class Stack<E> extends Vector<E> {

  public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }
}

public class Vector<E> ...생략 {

   public synchronized void removeElementAt(int index) {
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        modCount++;
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }
}

 

메모리 누수의 원인과 해결책


다 쓴 객체의 참조를 해제하지 않기 때문에 메모리 누수가 발생하는데 어떤 경우가 있는지 살펴보자.

 

💡자기 메모리를 직접 관리하는 클래스에서 제대로 관리는 못하는 경우

  • Java의 Stack 클래스처럼 자기 메모리를 직접 관리하는 경우 더 이상 참조가 되지 않는 객체들을 직접 참조 해제를 해주어야 합니다. 그렇지 않으면 예제처럼 가비지 컬렉터가 제대로 일을 수행못하게 됩니다.
  • 유효범위 밖이거나 쓸 일이 없어지는 객체는 개발자가 신경써서 null 처리를 해주어야 합니다.

💡캐시

  • 객체 참조를 캐시에 넣고 그 부분을 잊어버린다면 객체를 다 쓴 뒤에도 참조 해재게 되지 않기때문에 메모리에 쌓이게 됩니다.

💡리스터와 콜백

  • 클라이언트에서 콜백을 등록만하고 해제를 하지 않는다면 콜백은 쌓여만 갑니다.
  • WeahHashMap가은 콜렉션에 약한 참조로 저장하면 사용 후 가비지 컬렉터에 의해 수거됩니다.

 

 

 

참고자료)

https://catsbi.oopy.io/d7f3a636-b613-453b-91c7-655d71fda2b1

https://github.com/Meet-Coder-Study/book-effective-java/blob/main/2%EC%9E%A5/7_%EB%8B%A4%20%EC%93%B4%20%EA%B0%9D%EC%B2%B4%20%EC%B0%B8%EC%A1%B0%EB%A5%BC%20%ED%95%B4%EC%A0%9C%ED%95%98%EB%9D%BC_%EC%9D%B4%EC%A3%BC%ED%98%84.md

 

 

 

728x90
반응형