Programming/Java

[Youtube][이팩티브 자바] #7 불필요한 객체 레퍼런스를 정리하자

bisi 2020. 5. 24. 10:37

백기선님의 유투브 강의 내용을 정리하였습니다.

 

백기선님 Github 바로가기

Youtube 바로가기

 

 

강의내용 필기 


주제 7 : 더이상 쓰지 않는 개체 레퍼런스는 없애자 

 

메모리 직접 관리 

자바에 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() { // 초기화 할때 16만큼 만든다.
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        this.ensureCapacity();//용량확인
        this.elements[size++] = e;//size는 index의미에 가까움. //size++ 뒤에 있으면 쓴 다음 증가
        
    }

    public Object pop() {
        if (size == 0) {// index가 0이면 아무것도 없으니 emptyexcpeiton 발생시킴
            throw new EmptyStackException();
        }

		//0이 아닌경우에느 size를 줄임. //size 앞에 --를 붙여  하나 작은걸 가져옴
        return this.elements[--size]; // 주목!!
    }

    /**
     * Ensure space for at least one more element,
     * roughly doubling the capacity each time the array needs to grow.
     */
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}
스텍: 블록 쌓기, 먼저 들어간것이 나중에 나옴. LIFO FILO

 

  • 스택에 계속 쌓다가 많이 빼냇다고 치자. 그래도 스택이 차지하고 있는 메모리는 줄어들지 않는다. 왜냐하면 저 스택의 구현체는 필요없는 개체에 대한 레퍼런스를 그대로 가지고 있기 때문에다.
  • 즉 this.elements[--size]에는 계속 0을 들고 있다. return을 했지만, elements를 지워주는 게 아니다. 배열은 값을 넣기만하고, 실제로 빼진 않는다.
  • 가용한 범위는 size보다 작은 부분이고 그 값보다 큰 부분에 있는 값들을 필요없이 메모리를 차지 하고 있는 부분이다.
    • 가용한 범위 : 실제 elements를 담고 있는 사이즈, 유의미한 값들을 사이즈
    • 가용한 범위 밖에 있는 값들은 모두 비워두 된다. 

 

다음과 같이 코드를 수정할 수 있다. 

public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }
이렇게 코딩하면, 빼낼때마다 그자리에 있는 것을 null로 만든다.

 

 

스택에서 꺼낼 때 그 위치에 있는 개체를 꺼내주고 그 자리를 null로 설정해서 다음 GC가 발생할 때 레퍼런스가 정리되게 한다. 실수로 해당 위치에 있는 객체를 다시 꺼내는 경우에 NullPointerException이 발생할 수 있긴 하지만, 그자리에 있는 객체를 비우지 않고 실수로 잘못된 객체를 돌려주는 것 보다는 차라리 괜찮다. (당연한 에레어임~!!)프로그래밍 에러는 언제든지 가능한 한 빨리 포착하는 것이 유익한다.(Fail-Fast?)

 

그렇다고 필요없는 객체를 볼 때마다 null로 설정하는 코드를 작성하지는 말자. 객체를 Null로 설정하는 건 예외적인 상황에서나 하는것이지 평범한 일은 아니다. 필요없는 객체 레퍼런스를 정리하는 최선책은 그 레퍼런스를 가리키는 변수를 특정한 범위(스코프)안에서만 사용하는 것이다. 

public Object exmple() {
		Object age = 28;        
        age = null; //로컬 변수이기 때문에 해줄 필요가 없다.
        
        return age;
    }

 

로컬 변수는 그 영역 넘어가면 쓸모 없어져서 정리되기 때문이다.  변수를 가능한 가장 최소의 범위로 사용하면 자연스럽게 그렇게 될것이다. (하지만 위의 코드처럼 size는 멤버 변수를 elements를 쓰는 경우엔 역시 자연스럽게 그렇게 되진 않으니까.. 즉 예외적이 상황이라 그래서 명시적으로 null로 설정하는 코드를 써줘야하 했던것이다.

 

그럼 언제 레퍼런스를 null로 설정해야하는가? 메모리를 직접관리할때이다. 

Stack 구현체 처럼 elements라는 배열을 관리하는 경우 GC는 어떤 객체가 필요없는 객체인지 알 수 없다. 오직 프로그래머만 elements에서 가용한 부분(size보다 작은 부분)과 필요없느 ㄴ부분(size보다 큰부분)을 알 수 있다. 따라서 프로그래머가 해다 레퍼런스를 null로 만들어서 GC한테 필요없는 객체들이라고 알려줘야한다.

 

메모리를 직접 관리하는 클래스는 프로그래머가 메모리 누수를 조심해야한다.

 

 

캐시

캐시는 보통 키와 맵이다. 아래는 예시 .. 안좋은 코드!

public class CacheSample {

    public static void main(String[] args) {        
        Map<String, List> cache = new HashMap<>();
        
    }

}

 

캐시를 사용할 때도 메모리 누수 문제를 조심해야한다. 객체의 레퍼런스를 캐시에 넣어 놓고 캐시를 비우는 것을 잊기 쉽다. 여러 가지 해결책이 있지만, 캐시의 키에 대한 레퍼런스가 캐시 밖에서 필요 없어지면 해당 엔트리를 캐시에서 자동으로 비워주는 WeakHashMap을 쓸 수 있다. 

public class CacheSample {

    public static void main(String[] args) {
        Object key1 = new Object();
        Object value1 = new Object();

        Map<Object, Object> cache = new WeakHashMap<>();
        cache.put(key1, value1);
    }

}

WeakHashMap은 똑같이 key를 넣지만, key는 WeakHashMap으로 한번 감싸서 들어간다.

 

또는 특정 시간이 지나면 캐시값이 의미가 없어지는 경우에 백그라운드 쓰레드를 사용하거나 (아마도 ScheduledThreadPoolExecutor), 새로운 엔트리를 추가할 때 부가적인 작업으로 기존 캐시를 비우는 일을 할 것이다. 

(LinkedHashMap 클래스는 removeEldesEntry라는 메서드를 제공한다.)

 

 

콜백

세번째로 흔하게 메모리 누수가 발생할 수 있는 지점으로 리스너와 콜백이 있다.

 

클라이언트 코드가 콜백을 등록할 수 있는 API를 만들고 콜백을 뺄 수 있는 방법을 제공하지 않는다면, 계속해서 콜백이 쌓일 것이다. 이것 역시 WeahHashMap을 사용해서 해결할 수 있다.

 

메모리 누수는 발견하기 쉽지 않기 때문에 수년간 시스템에 머물러 잇ㅇ르 수도 있다. 코드 인스택션이나 heap profiler 같은 디버깅 툴을 사용해서 찾아야 한다. 따라서 이런 문제를 예방하는 방법을 학습하여 미연에 방지하는 것이 좋다.

 

우리 보통 만드는 객체들은 Strong Reference라고 보면 되고, WeakReference, SoftReference와 같은 클래스 개념이 있다. WeakReference를 Strong Reference를 한번 감싸서 만들어야한다.

 

예를 들어 아래와 같이 사용된다면 WeakReference로 가르키는 대상인 widget이 GC 대상이 될 수 있다. 

 

WeakReference weakWidget = new WeakReference(widget);