본문 바로가기

자바☕/이펙티브 자바

이펙티브 자바 읽고 정리해보기 7.

728x90

아이템 7 : 다 쓴 객체의 참조를 해제하라

public class Stack {
	private Object[] elements;
    private int size = 0;
    private static final int DEFALUT_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];
    }
    
    // 원소를 위한 공간 확보 메서드, 배열 크기 확장 시 기존 배열의 2배 확장
    private void ensureCapacity() {
    	if (elements.length == size)
        	elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

+ 제네릭 버전(아이템 29)

 

이 코드를 오래 실행하다보면, GC 활동과 메모리 사용량의 증가로 인해 메모리 누수 현상이 발생하게 된다. 심할 경우 디스크 페이징(다른 메모리를 추가로 할당해 프로세스에 대한 메모리 사용량이 증가하게 된다)이나 OOM의 원인이 될 수 있다

+

https://cdaosldk.tistory.com/264

2-1)의 페이징 참고하기

 

CS 강의 2. CPU와 메모리 심화

출처 : 내일배움캠프 1. CU의 핵심 기능 : 스케줄링 1) 스케줄링 소개 - 프로그램을 실행하는 주체 = 프로세스 ex) 카톡 실행 - 작업을 처리하는 주체 = 스레드 ex) 카톡 메세지 송수신 CPU를 잘 사용하

cdaosldk.tistory.com

 

위 예제 코드에서 GC는 스택에서 꺼내진 객체를 회수하지 않는다. 스택에 그 객체의 obsolete reference(다 쓴 참조, 만기 참조)를 갖고 있기 때문이다. pop() 메서드를 통해 해당 배열의 사이즈를 감소시켰지만, 이게 실제로 배열에 있는 객체 참조까지 감소시킨 것이 아니기 때문이다. 객체 참조 하나가 살아있다면, GC는 그 참조뿐 아니라 연관된 모든 객체 참조를 회수할 수 없다. 이는 성능에 악영향을 줄 가능성이 있다. 이는 참조를 다 썼을 때 null 처리 하는 것으로 대응이 가능하다

public Object pop() {
	if (size == 0)
    	throw new EmptyStackException();
    Object result = elements[--size];
    element[size] = null; // 다 쓴 참조해제
    return result;
}

 다 쓴 참조를 null 처리하면, 해당 null 처리된 객체를 재사용할 경우, NullPointerException이 발생하며 예상치 못한 버그를 예방할 수 있다

 

그러나 모든 메서드에서 객체 참조를 다 사용하고 난 후 null 처리하는 것은 매우 비효율적이다. 그보다 더 효율적인 방법은 다 쓴 객체 참조를 담고 있는 변수를 유효 범위 바깥으로 밀어버리는 것이다(+ 아이템 57)

Stack의 경우, 자신의 메모리를 스스로 관리하기 때문에 예외사항에 해당하는 것이다. 예제 stack의 경우, 객체가 아니라 객체 참조를 담는 elements 배열로 저장소 풀을 만들어 원소를 관리한다. 이 저장소 풀 안의 객체의 상황을 GC가 알 수 없으므로 프로그래머만이 그 상황을 관리할 수 있다

캐시 또한 메모리 누수의 주요 원인이다. 다 쓴 참조를 캐시에 넣고 별도 처리를 하지 않으면 메모리의 누수로 이어지게 된다. 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이라면, weakHashMap을 활용하면 된다. 다 쓴 엔트리는 즉시 회수된다

일반적으로 캐시 엔트리의 유효기간을 정확히 알기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 흔하게 사용한다. 이 경우 쓰지 않는 엔트리를 정리하는 것까지 구현해야 한다. ThreadPoolExecutor, 백그라운드 스레드를 활용하는 방법이나, 캐시에 새 엔트리를 추가 시 부수 작업을 수행하는 방법이 있는데 LinkedHashMap의 경우 removeEldestEntry를 활용해 새 엔트리 추가 시 기존 엔트리에 대한 작업을 수행한다(더 복잡한 캐시를 위할 경우 java.lang.ref 패키지를 활용하라)

메모리 누수의 다른 원인으로, 콜백 또는 리스너가 있다. 클라이언트가 콜백을 등록하기만 하고 해지하지 않는 경우, 콜백은 계속 쌓이기만 한다. 이럴 때 콜백을 약한 참조로 저장하면 GC가 즉시 수거한다(weakHashMap에 키로 저장한다)

 

메모리 누수에 대해 힙 프로파일러 등의 방법을 익혀 대비할 필요가 있다

 

728x90