[아이템 7] 다 쓴 객체 참조를 해제하라

gang_shik·2022년 2월 19일
0

Effective Java 2장

목록 보기
7/9
  • C, C++처럼 메모리를 직접 관리해야 하는 언어와 달리 자바의 경우 가비지 컬렉터가 다 쓴 객체를 알아서 회수를 함

  • 그렇다고 메모리 관리에 더 이상 신경 쓰지 않아도 되는 것은 아님

  • 스택을 예시로 들어볼 수 있음

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();
		}

		/**
		 * 원소를 위한 공간을 적어도 하나 이상 확보한다.
		 * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
		 */
		private void ensureCapacity() {
				if (elements.length == size) 
						elements = Arrays.copyOf(elements, 2 * size + 1);
		}
}
  • 위의 스택을 쓰는데 문제는 없음 하지만 사용하는 프로그램을 오래 실행하다보면 점차 가비지 컬렉션 활동메모리 사용량늘어나 결국 성능저하될 것임

  • 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료되기도 함

  • 위의 코드에 경우도 스택에서 꺼내진 객체들을 가비지 컬렉터회수하지 않음(그 객체를 더 이상 사용하지 않아도)

  • 이 스택은 그 객체들의 다 쓴 참조를 여전히 가지고 있음(다 쓴 참조는 앞으로 다시 쓰지 않을 참조를 뜻함)

  • 가비지 컬렉션에서 객체 참조 하나를 살려두면 그 객체뿐 아니라 그 객체가 참조하는 모든 객체회수해가지 못함

  • 그러면 여기서 단 몇 개의 객체가 매우 많은 객체회수되지 못하게할 수 있고 잠재적으로 성능에 악영향을 줄 수 있음

  • 여기서 풀 수 있는 해법은 null처리(참조 해제)하면 됨, 스택코드를 예로 들면 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 꺼내질 때임, 그 시점에 null 처리를 해주면 됨

public Object pop() {
		if (size == 0)
				throw new EmptyStackException();
		Object result = elements[--size];
		elements[size] = null; // 다 쓴 참조 해제
		return result;
}
  • 이렇게 null 처리를 함으로써 실수로 이 참조를 사용하면 바로 NullPointerException을 던지고 종료함, 프로그램 오류를 조기에 발견할 수 있음

디스크 페이징?

디스크 페이징

여기서 심한 경우 디스크 페이징이 일어난다고 하였는데 이 디스크 페이징이란 모자란 물리적 공간하드 디스크에서 활용해서 쓰는 것을 의미함

이게 왜 심각한 경우가 되냐면 단순히 메모리를 사용했을 때와 비교해서 사용량과 가비지 컬렉션 활동이 늘어나 속도가 안 그래도 느린데 메모리 영역하드 디스크에 일시적으로 저장되어 버리는 것임, 이는 메모리보다 하드 디스크에서의 속도가 월등하게 느리기 때문에 정말 이 영역까지 넘어오면 매우 심각한 상황임을 알 수 있음


  • 하지만 그렇다고 무조건 모든 객체를 쓰자마자 일일이 null처리 할 필요는 없음, 이로 인해 오히려 필요 이상으로 프로그램이 지저분하게 될 수 있음

  • 객체 참조를 null처리하는 일은 예외적인 경우여야 함, 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것임, 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이루어짐

  • null 처리의 경우 위와 같이 스택의 경우 자기 메모리를 직접 관리해서 가비지 컬렉터가 이 사실을 알 길이 없음, 그래서 비활성 영역 역시 유효한 객체로 보기 때문에 이를 null 처리해서 직접 알려줘야함

  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야함, 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null처리 해줘야함

  • 캐시 역시 메모리 누수를 일으키는 주범임, 객체 참조캐시에 넣고 잊어버리면 그냥 놔두는 경우가 생겨버림

  • 해법은 여러가지 인데 만약 운 좋게 캐시 외부에서 키를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만들면 됨, 그러면 다 쓴 엔트리는 그 즉시 자동으로 제거됨(단 이런 상황에만 유용함)

  • 캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리 가치를 떨어뜨리는 방식을 통해서 쓰지 않는 엔트리를 이따금 청소해줘야함

  • 아니면 백그라운드 스레드를 활용하서나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행하는 방법이 있음

  • 또 다른 주범은 리스너 혹은 콜백임, 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백이 쌓여감, 여기서 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해감

  • 이런 누수는 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 동원해야 발견됨, 미리 예방을 하는게 좋음

WeakHashMap? 약한 참조?

WeakHashMap을 사용해 캐시를 만든 것

먼저 엔트리는 캐시에서 메모리 내 여러 주소값 목록적재된 곳임, 그래서 이렇게 엔트리가 살아있는 상황에서 WeakHashMap을 쓴다는 표현을 한 것임

WeakHashMap은 Map의 형태이므로 Key와 Value 한 쌍으로 이루어져 있어서 이 상황에서 쓴다는 것임, 왜냐하면 WeakHashMap의 경우 Key에 대한 참조가 더 이상 존재하지 않게 되면 Value를 가져올 수 있는 방법이 없다고 판단하여 Key-Value 쌍자동으로 삭제되는 Map임

그래서 WeakHashMap을 사용해 캐시를 만들면 다 쓴 엔트리는 그 즉시 자동으로 제거됨이라고 말한 것임

약한 참조?

여기서 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거한다고 하였는데 여기서 왜냐하면 대상 객체를 참조하는 경우 약한 참조객체만 존재하면 가비지 컬렉터의 대상이 됨, 그래서 다음 가비지 컬렉터실행시 무조건 힙 메모리에서 삭제가 됨

어떻게 보면 WeakHashMap의 경우도 이런 케이스라고 볼 수 있음, 그래서 콜백을 이런 약한 참조를 걸어서 콜백이 쌓여가는 상황에 대해서 미리 방지하고 알아서 가비지 컬렉터가 수거하게끔 설정할 수 있는 것임, 이를 활용해서

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글