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

2021. 6. 23. 22:53책/이펙티브자바

Java의 경우 GC가 메모리 관리를 해준다.

그래서 메모리 관리에 더 이상 신경을 쓰지 않아도 된다고 오해할 수 있으나 절대 사실이 아니다.

// 일반적인 Stack 의 pop 메소드
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    return elements[--size];
}

특별한 문제는 없어 보일 수 있다. 하지만 '메모리 누수' 의 문제가 있는데 이 프로그램을 오래 실행하면 점차 GC 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다.

상대적으로 드문 경우긴 하지만 디스크 페이징이나 OutOfMemoryError를 일으킬 수 있다.

 

여기서 문제점은 다 쓴 참조(obsolete reference)를 여전히 가지고 있다는 점이다.

프로그램에서 그 객체들을 더 이상 사용하지 않더라도 말이다.

 

해결방안은 해당 참조를 다 쓰면 null(참조 해제)로 초기화 해주면 된다.

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

다 쓴 참조를 null 처리하는데 이에는 다른 이점도 따라온다.

만약 null 처리한 참조를 실수로 사용하려 하면 프로그램이 즉시 NullPointerException 을 던지며 종료한다.

 

그렇다면 모든 객체를 null로 해야할까 ?

모든 객체를 다 쓰자마자 일일이 null 처리하는 경우도 있는데 그럴 필요도 없고 바람직하지도 않다.

객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

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

 

메모리 누수

Stack 과 같이 자기 메모리를 직접 관리하는 클래스

Stack의 경우 자기 메모리를 직접 관리하는데 활성 영역과 비활성 영역 중 어느 부분이 사용되고 안되는지 GC의 입장에서는 알 수 가 없기에 메모리 누수가 발생할 수 있다.

해결방안

  • 프로그래머나 아는 사실이므로 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더 이상 쓰지 않을것임을 GC에 알려줘야한다.

캐시

객체 참조를 캐시에 넣고 다 쓴 이후에도 그냥 두는 경우가 있다.

해결방안

  • 캐시 외부에서 Key를 참조하는 동안만 엔트리가 살아있는 캐시가 필요한 경우는 WeakHashMap 을 사용한다.
  • 엔트리의 유효기간을 정해둔다.
    • 사실상 계산하는것이 어렵다
  • 쓰지 않는 엔트리를 청소한다.
    • ScheduledThreadPoolExecutor와 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할때 부수 작업으로 수행하는 방법을 이용한다.
    • LinkedHashMap의 경우 removeEldestEntry 메서드를 사용해 후자의 방식으로 처리한다.
  • 더 복잡한 캐시를 만들고 싶을때는 java.lang.ref 패키지를 직접 활용한다.

콜백

  • 콜백이란 이벤트가 발생하면 특정 메소드를 호출해 알려주는 것입니다.(1개)
  • 리스너는 이벤트가 발생하면 연결된 리스너(핸들러)들에게 이벤트를 전달합니다.(n개)
  • 클라이언트가 콜백을 등록만 하고 해지하지 않는다면 콜백은 쌓이게 될 것이다.
  • 이럴 때 콜백을 약한 참조(weak reference)로 저장하면 GC가 즉시 수거해간다.
  • 예를 들어 WeakHashMap에 키로 저장해두면 된다.

결론

  • 메모리 누수를 방지하는 방법은 다쓴 객체 참조를 null로 처리하는 것과 지역변수의 범위를 최소화 하는 방법이다.
  • 모든 것을 null로 처리한다고 해서 좋은 것은 아니다. 가장 좋은 방법은 지역 변수의 범위를 최소화 하는 방법이다.
  • 메모리 누수의 주범은 자기 메모리를 직접 관리하는 경우, 캐시, 리스너, 콜백이다.
  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.
  • 메모리 누수는 철저한 코드리뷰, 힙 프로파일링 도구를 통해 디버깅을 해야 발견할 수 있기 때문에 메모리 누수를 철저히 신경써야 한다.