본문 바로가기

Reading/Effective Java

[Effective-Java] Item 7. 다 쓴 객체 참조를 해제하라 (+Java Reference와 GC)

자바에 가비지 콜렉터가 있어서 메모리 관리에 더 이상 신경쓰지 않아도 된다고 생각하는데, 이는 아니다. 

 

다 쓴 객체는 GC가 회수할 수 있게 null 처리하자


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;
    }
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = this.elements[--size];
        //인스턴스가 직접 사용하는거니까 null처리(참조 해제) 해야한다.
        this.elements[size] = null;
        return result;
    }
    // 원소를 위한 공간을 적어도 하나 이상 확보한다. 배열 크기를 늘려야 할 때마다 대략 2배씩 늘린다.
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

 

메모리 누수는 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

위 Stack.pop() 메소드가 this.elements[size] = null;을 갖지 않으면, 대량 추가후 대량 삭제하면 그대로 메모리 누수로 이어진다.

위 코드에서 pop() 메소드에 삭제할 다 쓴 객체를 null처리하여 참조 해제해야한다. size만 줄이면 여전히 Stack 객체는 다 쓴 객체(element)를 참조하고 있기때문이다. 다 쓴 참조를 null 처리하면 실수로 접근을해도 NPE를 던지며 종료할 수 있다는 이점도 있다.

 

객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체...)를 회수해가지 못하니 단 몇개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고잠재적으로 성능에 악영향을 줄 수 있다.

프로그램 오류는 가능한 한 조기에 발견하는게 좋다.

 

 

객체 참조를 null 처리하는 것은 예외적인 경우이다.


모든 객체를 다 쓰자마자 일일이 null 처리하는 것은 프로그램을 필요이상으로 지저분하게 만든다.

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

아래의 Integer a는 로컬 변수기 때문에 넘어가면 쓸모없어져서 GC에 자동으로 대상이된다.

 

public void hello(){
	Integer a = 1;
}

 

위 Stack 클래스가 메모리 누수에 취약한 이유는 자기 메모리를 직접 관리하기 때문에, 예외적인 상황이라 null 처리한 것이다.

이 Stack 클래스는 elements 배열로 저장소 풀을 만들어 원소들을 관리한다.

 

활성 영역에 속한 원소들이 사용이 되고, 비활성 영역은 쓰이지 않는다. 여기서 문제점이 GC는 알 길이 없다. GC 입장에서는 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체다. 쓸모없다는 것은 프로그래머만이 안다.

 

따라서, 비활성 영역이 되는 순간 null 처리해서 해당 객체를  더이상 쓰지않을 것이라고 GC에 알려야한다.

 

 

메모리 누수의 주범


1. 자기 메모리를 직접 관리하는 클래스

원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해주자.

 

2. 캐시

객체 참조를 캐시에 넣은 후, 잊은 채 그 객체를 다 쓴 뒤로도 한참을 그냥 놔두는 경우에 발생한다.

  • 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하면 WeakHashMap을 사용하자. 다 쓴 엔트리는 즉시 자동으로 제거된다. 단, WeakHashMap은 이러한 상황에서만 유용하다는 것을 기억하자.
  • 캐시 엔트리 유효기간을 정확히 정의하기 어려우니 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 사용한다면 엔트리를 이따금 청소를 해줘야한다.
    • ScheduledThreadPoolExecutor 등의 백그라운드 스레드를 활용하기
    • LinkedHashMap의 removeEldestEntry 메소드로 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하기
  • 더 복잡한 캐시를 만들고싶다면 java.lang.ref 패키지를 직접 활용하기

3. 리스너(listener) or 콜백(callback)

클라이언트가 콜백을 등록만하고 명확히 해지하지 않는다면, 콜백은 계속 쌓인다.

약한 참조(weak reference)로 저장하면 GC가 즉시 수거해갈 수 있다. 즉 WeakHashMap에 키로 저장하자.

 

메모리 누수는 발견하기 쉽지 않아 시스템에 수년간 잠복하는 사례도 있다. 철저한 코드리뷰, heap profiler 같은 디버깅 툴을 사용해서 찾아야 한다. 따라서 이런 문제는 예방법을 익혀두는게 매우 중요하다.

 

 

 

Java Reference 와 GC


 

참조: https://d2.naver.com/helloworld/329631

 

기본적으로 자바의 GC는 heap내의 객체 중 가비지(garbage)를 찾아내고 찾아낸 가비지를 처리해서 힙의 메모리를 회수한다.

java.lang.ref 패키지를 이용하면 LRU(Least Recently Used) 캐시 같이 특별한 작업을 하는 애플리케이션을 더 쉽게 작성할 수 있다.

 

reachability

 

Java GC는 객체가 가비지인지 판별하기 위해서 reachability라는 개념을 사용한다.

  • reachable: 어떤 객체에 유효한 참조가 있을 때
  • unreachable: 어떤 객체에 유효한 참조가 없을 때

이 때, unreachable 객체를 GC가 가비지로 간주해 수거한다.

유효한 참조 여부를 파악하기위해 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.

Strong Reference는 일반적으로 new를 통해서 객체를 생성하게 되면 생기게 되는 참조다.

따로Reference를 상속하는 클래스가 존재하지 않는 일반적인 객체 생성 참조다.

 

 

Weak Reference

 

WeakReference<Sample> wr = new WeakReference<Sample>( new Sample());  
Sample ex = wr.get();  
...
ex = null;

 

WeakReference 클래스의 객체는 new() 메서드로 생성된 Sample 객체를 캡슐화한 객체이다.

참조된 Sample 객체는 두 번째 줄에서 get() 메서드를 통해 다른 참조에 대입된다.

이 시점에서는 WeakReference 객체 내의 참조와 ex 참조, 두 개의 참조가 처음 생성한 Sample 객체를 가리킨다.

이때 ex = null 시점으로 오면 weakly reachable상태가된다. 즉 Sample Object를 바라보는건 WeakReference 하나만

있는 상황이 되는 상태다. WeakReference 내의 참조가 null로 설정되고 GC는 weakly reachable 상태가되면 회수 대상으로 간주한다.

LRU 캐시와 같은 애플리케이션에서는 weakly reachable 객체가 유리하므로 대체로 WeakReference를 사용한다.

 

ReferenceQueue

 

WeakReference 객체가 참조하는 객체가 GC 대상이 되면 WeakReference 객체 내의 참조는 null로 설정되고 WeakReference 객체 자체는 ReferenceQueue에 enqueue된다. 후속적으로 처리를 하기위해서 Reference Queue를 사용할 수 있다.

 

 

WeakHashMap

 

위 내용들로 이해하자면 Entry를 만들어 넣을 때 대상 키값을 WeakReference로 감싸서 넣는다. 그리고 실제로 외부에서 참조가 해제가 되면 weakly reachable 상태가 되고, GC에 의해서 참조가 해제되며 WeakHashMap 내의 ReferenceQueue로 enqueue된다.

이 때 WeakHashMap에서 이 객체들이 있는 Entry[] table이 있는데 getTable()을 할때나, forEach 등 다양한 메소드를 부를 때

expungeStaleEntries() 메소드를 호출하는데, 이때 ReferenceQueue에 있는 객체들을 poll하면서 실제 table에서도 삭제하며

이 Entry도 null로 만들면서 참조 자체를 지운다. WeakHashMap이 가진 키값이 외부에서 참조가 끊겼을 때 실제 키 값도 삭제할 수

있는 이유가 이 것 때문이다.

 

 

이 Reference와 GC는 정말 간략하게 필요한 부분만 정리했음으로,

좀더 자세한 Reference들과 GC의 과정등 다양하게 알기 위해서 위 참조링크를 꼭 활용하자

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.