[TIL] Effective Java - Item.07

김주형·2023년 2월 27일
0

TIL

목록 보기
30/37
post-thumbnail

Reference 🙇🏻‍♂️

'GC를 갖춘 언어를 사용하면 편리하지만, 메모리 관리에 더 이상 신경쓰지 않아도 되는 것은 아니다.'


메모리 누수

메모리 누수 (memory leak)

  • 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상
  • 할당된 메모리를 사용 후 반환하지 않는 것이 누적되면 메모리가 낭비된다.
  • 스택을 통한 직접 메모리를 관리하는 경우의 예시
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 e) {
		ensureCapacity();
		elements[size++] = e;
	}

	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);
		}
	}
}
	
  • 메모리 누수로 인해 메모리 사용량이 늘어나 성능 저하가 일어날 것입니다.
    - 심한 경우 디스크 페이징 또는 OutOfMemoryError가 발생할 수 있습니다.
  • pop() 메서드를 살펴보면 줄어드는 경우에 스택에서 꺼내진 객체들은 GC가 되지 않습니다.
    - size 변수만 줄어들고 실제로 배열 안에 값들은 남아있기 때문
    - 값이 남아 있는 부분은 다 쓴 참조(obsolete reference)에 해당됨
    	>- 다 쓴 참조(obsolete reference : 앞으로 다시 쓰지 않을 참조를 의미
  • 객체 참조 하나를 살려둔다면 GC는 그 객체 뿐만 아니라 해당 객체가 참조하는 모든 객체를 회수할 수 없습니다.

다 쓴 참조로 인한 메모리 누수 해결방법 - 직접 메모리를 관리하는 클래스의 경우

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

	return result
}

// Vector 클래스의 removeElementAt
public synchronized void removeElementAt(int index) {
	...
	elementCount--;
	elementData[elementCount] = null; /* to let gc do its work */
}
  • 가장 간단한 방법은 참조를 다 사용하고 난 후 null 처리를 통한 참조해제를 하면 됩니다.
  • 부가적으로 null 처리한 참조를 사용하려고 한다면 프로그램은 NPE 발생으로 빠른 오류 발견이 보장됩니다.
  • 자기 메모리를 직접 관리하는 클래스가 있다면 객체 참조를 null로 처리하는 방법을 사용하여 메모리 누수에 더더욱 주의해야 합니다.

다 쓴 참조로 인한 메모리 누수 해결방법 - 일반적인 경우

  • 일반적인 경우에도 null 처리를 한다면 프로그램이 필요 이상으로 지저분해집니다.
  • 가장 바람직한 방법은 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것입니다.
  • 변수의 범위를 최소가 되게 정의하고 해당 스코프 안에서만 사용한다면 누수문제는 자연스럽게 해결됩니다.

캐시

  • 캐시를 사용하는 경우에도 메모리 누수를 주의해야 합니다.
  • 보통 객체 참조를 캐시에 넣고 시간이 지나는 경우, 메모리 누수가 일어납니다.

캐시로 인한 메모리 누수 해결

@Test
void weakHashMapTest() {

	weakHashMap<Integer, String> map = new weakHashMap<>();

	Integer key1 = 1000;
	Integer key2 = 2000;

	map.put(key1, "Orange");
	map.put(key2, "Coffee");

	key1 = null;

	System.gc();

	map.entrySet().stream.forEach(e1 -> System.out.println(e1));
	assertThat(map.size()).isEqualTo(1);
}
  • 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요한 상황이라면, 사용하고 난 엔트리를 제거해주는 WeakHashMap을 사용해 캐시를 생성할 수 있습니다.
  • 보통 시간이 지날 수록 캐시 엔트리의 가치를 감소시킨 후, 가치가 없어진 엔트리를 제거해주는 방법을 사용한다고 합니다.
    - 백그라운드 스레드를 사용하여 캐시를 지우는 방법 -> 예) ScheduledThreadPoolExecutor
    - 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법 -> 예) LinkedHashMap

콜백

  • 클라이언트가 콜백을 등록만 하고 해제하지 않는다면 콜백은 계속 누적됩니다.

콜백의 메모리 누수 해결 방법

  • 콜백을 WeakHashMap을 이용하여 약한 참조로 저장한다면 사용하지 않는 경우 GC가 수거해간다고 합니다.

정리

  • 메모리 누수는 확인하기 쉽지 않나 오랫동안 잠복하는 경우가 있다.

  • 메모리 누수가 일어나는 패턴들을 알아두고 대처 방법을 익혀두는 것이 매우 중요하다.

profile
근면성실

0개의 댓글